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
Nachteile
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.
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
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.
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:
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.
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:
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.
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:
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.
Kategorien: SharePoint, Sonstiges, Technical
Schlagwörter: Frontendentwicklung, JavaScript, Node.JS, Performance, SharePoint 2013
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!