06. Dezember 2013 · von Joerg Sager

Content Delivery Network – Performance für den SharePoint bei der Frontendprogrammierung

Auch die Technologien stehen nicht still und so wandelt sich die Art und Weise, wie wir unsere individuellen Entwicklungen vornehmen. Dank immer schnellerer und wachsender JavaScript Engines verlagern sich immer mehr Teile der Entwicklung ins Frontend.

Diesem Trend folgt auch der SharePoint 2013 und bietet ein ausgereiftes JavaScript client object model, mit dessen Hilfe Benutzer-Aktionen über die SharePoint 2013 client API abgesetzt werden können. Doch wo sind die Vor- und Nachteile auf die Schnelle erkennbar?

Vorteile

  • Neuladen von Seiten kann durch AJAX-Calls vermieden und somit die gefühlte Wartezeit für den Benutzer reduziert werden.
  • Man entwickelt im neuen Webstandard mit HTML5 und JavaScript. Für die Entwicklung von Live-Kacheln oder Windows-Store Apps wir zum Beispiel die Verwendung von HTML5 und JavaScript empfohlen.
  • Kein Ausrollen von WSP-Solutions oder anderen Deploymentprozessen.

Nachteile

  • Das Debugging von clientseitigem Scripting ist manchmal schwierig.
  • Die Performance kann durch viele JavaScript Dateien und Rendering Delays sinken.
  • JavaScript bietet von Haus keine konsequente Typisierung sondern arbeitet mit dynamischer Typisierung beim Zuweisen von Werten. Hier durch können mehr Fehler entstehen.

Abhilfe

Typisierung in JavaScript

Es gibt verschiedenste JavaScript Erweiterungen, welche auf Wunsch Klassen, Interfaces, Vererbung, Module, anonyme Funktionen, Generics und eine statische Typisierung ermöglichen. Die bekannteste Erweiterung im SharePoint Umfeld für die Typisierung ist TypeScript, welches von Microsoft entwickelt wurde. TypeScript ist abwärtskompatibel zu gängigen JavaScript-Bibliotheken.

Debugging von JavaScript

Lange Zeit gab es ein Problem mit gängigen Entwicklungsumgebungen JavaScript zu debuggen. Mittlerweile bietet der Internet Explorer im Zusammenspiel mit Visual Studio die Möglichkeit JavaScript Code zu debuggen. Hierzu muss im Internet Explorer das Debugging eingeschaltet werden.

Aktivieren der Debugging-Möglichkeit im Internet Explorer
1) Im Menü auf die Internet Optionen klicken.
2) Im Fenster der Internet Optionen auf den Reiter Erweitert klicken.
3) Unter der Kategorie Browsen muss der Haken von Skriptdebugging deaktivieren (Internet Explorer) entfernt werden.
4) Das Fenster Internet Optionen mit OK bestätigen und den Internet Explorer neustarten.

Wenn man jetzt im Code den Befehl

debugger;

verwendet, dann verbindet sich Visual Studio automatisch mit dem Script und ermöglicht das Debugging für JavaScript Dateien.

Weitere Informationen auf Debuggen von clientseitigen Skripts

Performance

Aus unseren Erfahrungen heraus sind die zwei häufigsten Fehler folgende:

1) Die Menge der JavaScript Dateien
Bei einem größeren Projekt kann die Menge der JavaScript Dateien schnell eine kritische Masse überschreiten. Es werden weitere Frameworks hinzugeladen wie jQuery, jQuery UI, knockout.JS oder ähnliche Frameworks. Zusätzlichen werden eigene JavaScript Dateien angelegt, um den eigenen Code vorzuhalten. Äquivalentes Verhalten entsteht mit CSS-Dateien.

Die gängigen Webbrowser unterstützen nur eine geringe Zahl an parallelen Verbindungen zwischen Client und Webserver. Wenn neben den SharePoint CSS-, Bilder-, JavaScript Dateien noch weitere 5-10 Dateien hinzukommen können schnell Performance-Probleme entstehen.

Begründet ist dieses Problem durch folgendes Verhalten der Webbrowser: Ein Browser kann nur eine bestimmte Anzahl von Verbindungen parallel zum Webserver aufbauen. Weitere Verbindungen werde solange zurück gestellt bis die laufenden Verbindungen abgeschlossen sind. Zusätzlich muss sich jeder Request wieder neu am Webserver authentifizieren.

Die folgende Tabelle zeigt die maximalen Verbindungen zu einem Webserver.

Browser HTTP/1.1
IE 6,7 2
IE 8 6
Firefox 2 2
Firefox 3 6
Safari 3,4 4
Chrome 1,2 6
Chrome 3 4
Chrome 4+ 6

2) Die Menge der JavaScript Rendering Interrupt Operationen

Bei Content Manipulationen, gerade wenn Änderungen vorgenommen werden bevor die Seite fertig gerendert ist, entstehen Performance Verluste. Hierzu zählen insbesondere das Hinzufügen von DatePickern, Dialogen oder auch jQuery UI Tabs. Alle diese fortführenden Manipulationen laufen bevor die Seite vollständig im Webbrowser dargestellt wird.

Was kann man bei zu vielen JavaScript und CSS Dateien tun?

Zum Einen ist es bei vielen Dateien die Einführung eines Content Delivery Network sinnvoll. Dies ist ein Webserver mit einem eigenem DNS-Namen, welcher nur den Zweck erfüllt, Dateien wie JavaScript, CSS oder auch Bilder komprimiert zu verteilen. Somit kann man das Problem der maximalen Verbindungen pro Server umgehen und mit mehren Requests parallel arbeiten.

Mit einer Netzwerkanalyse kann man diesen Effekt gut darstellen:

SharePoint 2013 viele JavaScript Dateien

 

In die SharePoint Seite wurden 12 JavaScript Dateien zusätzlich hinzugefügt. Durch NTLM-Handshake gehen bis zu 1000 ms (1 Sekunde) pro Abfrage an Performance verloren.

SharePoint 2013 eine Anfrage

Komprimiert man die zwölf JavaScript Dateien in eine einzige Datei, dann liegt bedingt durch die Größe die Ladezeit nur bei 300ms. Nutzt man einen authentifizierungsfreien Webserver kann das Laden der Datei, je nach Größe, auf 7-10 ms verkürzt werden.

In diesem Vergleich verlieren wir über 1 Sekunde an Performance. Beim Laden mit der zusammengefassten JavaScript Datei braucht die Seite 4,1 Sekunden. Beim Laden von zwölf JavaScript Dateien haben wir eine Ladezeit von 5,85 Sekunden. Somit wurde fast 1,8 Sekunden beim Laden der SharePoint Seite eingespart.

Das Komprimieren von vielen Dateien bietet Entlastung und spart Ressourcen durch weniger Verbindung an den Webserver. Die Seite lädt schneller, wenn Authentifizierungs-Handshakes wegfallen.

Da Bilder, CSS oder auch JavaScript Dateien in den meisten Fällen keinen sicherheitsrelevanten Inhalt darstellen, ist es in diesem Fall sinnvoll komplett auf eine Authentifizierung zu verzichten.

In diesem Beispiel bestehen an das CDN (Content Delivery Network) folgende Anforderungen:

  1. Authentifizierungslos
  2. Komprimierung und Zusammenfassen von Dateien
  3. Schnell und ohne viel Overhead

Wir haben uns bei diesen Anforderungen für einen Node.JS Webserver entschieden, welcher auf dem gleichen Server mitläuft. Selbstverständlich ist auch ein Applikation Pool / Webseite im IIS denkbar.

Neben dem Webserver werden zusätzlich die JavaScript- und CSS-Dateien mittels eines JavaScript Compressors komprimiert und verschlankt. Hierfür gibt es verschiedenste Tools, unter anderem den Closure Compiler von Google, welcher allerdings auf Java Technologie basiert. Wir haben uns für uglify-js für Node.JS entschieden, da es komplett auf JavaScript basiert und als Node.JS Modul einfach hinzugefügt werden kann.

Unser CDN (Content Delivery Network) auf Node.JS Basis:

Webserver und Initiator

CDN-WS.js

// ########## config ##########

// WebServer Port
var port = 1337;
// Path to JS and CSS Files
var jscsspath = "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\CustomJS";
// How often the cache should be remake in Seconds
var cachetime = 1;

// ########## config ##########

// ########## DON'T CHANGE ANYTHING BEHIND THIS LINE 
// init required modules
var fs = require('fs');
var http = require('http');
var url = require('url');

// custom modules
var s = require("./CDNFunctions.js");

// prepare start
if(!fs.existsSync("cf.js")){
	s.refreshCache(jscsspath);
}else if(!fs.existsSync("cf.css")){
	s.refreshCache(jscsspath);
}

	// check if cache should be refreshed
	function checkCache(){
		fs.readFile("cf.css", 'utf8', function(err, data){
			var cachedtime = data.toString().substring(2,15);
			if(!data)return;
			var actDate = new Date().getTime();
			if((actDate-cachedtime) > cachetime){
				s.refreshCache(jscsspath);
			}
		});
		// next refresh in 86400000 Milliseconds
		setTimeout(checkCache, 86400000);
	}
	// - check if cache should be refreshed

	// refresh cache after programstart in 10 Seconds
	setTimeout(checkCache, 10000);

	// create web server
	http.createServer(function(request, response) {
        try
        {
		  // get requested url
		  var pathname = url.parse(request.url).pathname.toString();
		  var raw;
		  var contentType = "";
		  // if js file requested
		  if(pathname == "/js"){
		   contentType = "application/x-javascript";
		   raw = fs.createReadStream('cf.js',{
											  flags: 'r',
											  encoding: 'utf8',
											  fd: null,
											  mode: 0666,
											});
		 // if css file requested
		  }else if(pathname=="/css"){
		   contentType = "text/css";
		   raw = fs.createReadStream('cf.css',{
											  flags: 'r',
											  encoding: 'utf8',
											  fd: null,
											  mode: 0666,
											});
		// error page
		  }else{
		    contentType = "text/plain";
			raw = fs.createReadStream('error.cf',{
											  flags: 'r',
											  encoding: 'utf8',
											  fd: null,
											  mode: 0666,
											});
		  }

		  // set encoding and set output 
			response.writeHead(200, {'Content-Type': contentType});
			raw.pipe(response);
        }
        catch(ex)
        {
            response.writeHead(401, {'Content-Type': 'text/plain'});
            response.end('Internal Error: ' + ex, 'utf8');
        }
	}).listen(port);

CDNFunctions.js

var fs = require("fs");

// concat and minify files
var readf = function(filelist, typ, done){
	var concat = "";
	var array = filelist.slice(0);
	// delete not focused files 
	for(curElem = array.length - 1; curElem >= 0; --curElem)
	{
		if(array[curElem].toLowerCase().indexOf(typ) == -1)
		{
			array.splice(curElem, 1);
		}
	}

	// Minify JavaScript files
	if(typ == '.js'){
		var UglifyJS = require("uglify-js");
		var result = UglifyJS.minify(array);
		done(null,result.code);
	// concat other files
	}else{
		var pending = array.length;
		if (!pending) return done(null, concat);
			array.forEach(function(file){
				var data = fs.readFileSync(file, 'utf8');
				concat+=data;
				if (!--pending) done(null, concat);
		});
	}
}

// find recursive js & css files in path
var walk = function(dir, done) {
  var results = [];
  fs.readdir(dir, function(err, list) {
	if (err) return done(err);
	var pending = list.length;
	if (!pending) return done(null, results);
	list.forEach(function(file) {
	  file = dir + '/' + file;
	  fs.stat(file, function(err, stat) {
		if (stat && stat.isDirectory()) {
		  walk(file, function(err, res) {
			results = results.concat(res);
			if (!--pending) done(null, results);
		  });
		} else {
		  if(file.indexOf('.css') != -1 || file.indexOf('.js') != -1){
			results.push(file);
		  }
		  if (!--pending) done(null, results);
		}
	  });
	});
  });
};

// read cached file
exports.readCachedFile  = function(filename){
	 if(!fs.existsSync(filename)) return;
	 return fs.readFileSync(filename, 'utf8');
}

exports.refreshCache = function(dir){
	walk(dir, function(err, results) {
	  if (err) throw err;		
		  console.log("preparing cache...");

		  // generate cached file css
		  readf(results, '.css', function(err, result){
				var date = new Date().getTime();
				result = "/*"+date+"*/n"+result;
				fs.writeFileSync("cf.css", result, 'utf8');

		  });

		  // generate cached file js
		  readf(results, '.js', function(err, result){
				var date = new Date().getTime();
				result = "/*"+date+"*/n"+result;
				fs.writeFileSync("cf.js", result, 'utf8');
		  });

	});
}

Das Ergebnis sind zwei Dateien. Eine cf.js in dem alle JavaScript Dateien komprimiert zusammgefasst werden. Die zweite Datei ist die cf.css in dem alle CSS Dateien zusammengefasst werden. Der Node.JS Webserver liefert beide Dateien unter http://servername:1337/js oder http://servername:1337/css aus.

Anmerkung: Das Auslagern von Bildern oder auch das Zusammenfassen von Icons Sets mit Hilfe von CSS Sprites bringt ebenfalls große Performance Vorteile. Der Ansatz ist nahezu gleich: man baut eine große Grafik und fässt mehre kleinere Icons zusammen. Dann zeigt man später nur einen Sichtbereich auf dem gesamten Bild an. Dieses verbessert die Ladezeit und reduziert die Anfragen an den Webserver.

Auch das Arbeiten mit Cache-Control und Expire-Attributen im HTTP Header kann Vorteile beim Laden von Dateien bringen, welche sich nicht häufig ändern.

Fazit
Der Einsatz eines CDN (Content Delivery Network) ist bei vielen selbst erstellten JavaScript- und CSS Dateien eine sinnvolle Alternative, welche zusätzlich in den SharePoint hinzugefügt werden. Es bringt messbare Performancevorteile und reduziert die Zeit bis die Seite im Webbrowser angezeigt wird deutlich.

Was kann man mit Rendering Interrupt Operations machen?

Ist das Antwortverhalten vom Server schnell, aber die Seite baut sich aber trotzdem langsam auf? Dieses kann an JavaScript Funktionen liegen, welche die Seite manipulieren bevor sie ausgeliefert wird.

Identifizieren von langsamen Funktionen

Mit Hilfe der Developer Tools im Internet Explorer (F12) besteht die Möglichkeit die Webseite mit einem Profiler zu Analysieren. Das Ergebnis sind die einzelnen JavaScript Funktionen mit Häufigkeit des Aufrufs und die Laufzeit.

Beispiel Ansicht des Internet Explorer Profiler:

Internet Explorer Profiler

Hat man die langsamen Funktionen identifiziert kann man versuchen diese Funktion zu verschlanken oder zu beschleunigen.

Lösungen

Es gibt keine generelle Lösung für JavaScript Performance Probleme, da die meisten Probleme individuelle Rahmenbedingungen haben.

Es ist in jedem Fall sinnvoll im Vorfeld zu überlegen, ob einige Operationen zeitversetzt durchgeführt werden können. Mit den JavaScript Timing Events kann man einige Operationen oder Daten zeitgesteuert nachziehen.

Beispiel:

Die Funktion FillDatePickerFields() füllt alle DatePicker-Input-Felder mit dem Datum von Heute. Startet man die Funktion mit einer Sekunde zeitversetzt nach dem Body.onload-Event mit setTimeout(‚FillDatePickerFields()‘, 1000); , dann ist es für den Benutzer kaum sichtbar. Der Benutzer bekommt die Seite schneller angezeigt, obwohl im Hintergrund noch Operationen laufen.

Hierbei unterschiedet man zwischen einer gefühlten Performance und einer technischen Performance. Der Fokus liegt meistens darin, dass der Benutzer schnellstmöglich Inhalt angezeigt bekommt auch wenn Dieser noch nicht fertig ist. Schaut der Benutzer dagegen längere Zeit auf eine weiße Seite entsteht schneller Ummut über die Performance, als wenn die Seite schneller angezeigt wird. Der Benutzer hat eine Reaktionszeit von über zwei Sekunden. Wenn die Seite geladen ist bleibt somit ein Zeitfenster von mindestens zwei Sekunden, um weitere Operationen durchzuführen bevor der Benutzer mit dem Arbeiten beginnt.

Fazit

Um Probleme zu erkennen und dagegen anzugehen bietet der Internet Explorer mit seinem eingebauten Profiling-Tool ein gutes Werkzeug. Ein frühzeitige Analyse von möglichen Performance-Engpässen und die daraus abgeleiteten passenden Maßnahmen helfen Performanceprobleme erst gar nicht entstehen zu lassen.



Diesen Blogeintrag bewerten:


Haben Sie Fragen zu diesem Artikel oder brauchen Sie Unterstützung?

Nehmen Sie mit uns Kontakt auf!

Wir unterstützen Sie gerne bei Ihren Vorhaben!


Schreibe einen Kommentar

Kontakt.
Lassen Sie sich von uns beraten
Wir freuen uns über Ihr Interesse an unseren Leistungen. Hinterlassen Sie
uns Ihren Namen, Ihre Telefonnummer und E-Mail Adresse – wir melden
uns schnellstmöglich bei Ihnen.
Kontakt aufnehmen