Softwareentwicklung und Managed Hosting
ANEXIA
AUG
23
2017

Mit PJAX die Webseiten-Performance steigern

Geschrieben am  23. August 2017 von Manuel Wutte

Viele von euch werden sich als Erstes vermutlich folgende Frage stellen: PJAX? Sollte das nicht eher AJAX heißen?

Die Antwort hierauf lautet klar Nein – bei PJAX handelt es sich um eine speziellere Art der Datenorganisation, basierend auf AJAX.
Was es aber konkret damit auf sich hat, und was euch diese Technik in der Praxis wirklich bringt, möchte ich euch hier zeigen. Dafür sind allerdings zumindest durchschnittliche Kenntnisse in Javascript bzw. jQuery erforderlich! Wenn ihr jedoch nicht so viel Erfahrung mit jQuery haben solltet: kein Problem – weiter unten gibt’s ein fertiges Beispiel, welches ihr als Ausgangsbasis für euer eigenes Projekt nutzen könnt.

 

PJAX im alltäglichen Leben

Wenn du beispielsweise Facebook, Twitter oder auch LinkedIn verwendest, ist dir sicher schon einmal aufgefallen, dass du zwar alle möglichen Links und Buttons anklicken kannst, sich die Website dabei aber nie komplett neu lädt. Wenn dir das bewusst ist, dann hast du bereits begriffen, worum es bei der PJAX-Technik eigentlich geht – dem Austauschen und Nachladen von Inhalten innerhalb bestimmter definierter Bereiche einer Website. Das bedeutet, dass die Website beim ersten Zugriff einmalig komplett geladen wird. Wenn dann irgendwo geklickt wird, wird lediglich der Inhalt mit jenem der eigentlichen Zielseite ausgetauscht, und so nebenbei ändert sich auf die URL in der Browser-Zeile.

Der Vorteil liegt nun darin, dass einzelne Seiten einer Website dadurch wesentlich schneller geladen werden können, da nicht permanent alle (zum Teil externen) Ressourcen jedes Mal aufs Neue nachgeladen werden müssen. Für den User selbst wirkt das Ganze dadurch wesentlich ruhiger und auch flüssiger, zumal durch das wegfallende Nachladen und den damit verbundenen Neuaufbau der Seite das entstehende Flackern komplett wegfällt.

 

PJAX in die Website integrieren

Die Integration der PJAX-Technik gestaltet sich verhältnismäßig einfach. Es lassen sich damit auch ältere Websites aufrüsten. Alles was dafür notwendig ist, wird mit wenigen Javascript bzw. jQuery-Funktionen gelöst.

 

Schritt 1: Events delegaten

Event-Listener auf Javascript-Basis funktionieren normalerweise so, dass diese eine Funktion bzw. ein Verhaltensmuster definieren, welches bestimmt, was bei einem bestimmten Auslöser (z.B. ein Klick) mit dem betreffenden Element (z.B. ein Button) geschehen soll. Diese Funktion wird für gewöhnlich direkt an das jeweilige Element gebunden. Dies geschieht, indem der Browser bei einem Seitenaufruf alle Elemente der Reihe nach rendert, und wenn er damit fertig ist, die notwendigen Event-Listener an diese bindet.

Da bei PJAX (oder teilweise auch bei AJAX) Elemente neu geladen, und dadurch überschrieben werden, werden in diesem Fall auch alle Event-Listener (vom alten Element) ebenfalls überschrieben. Der Browser selbst führt aber keine erneute Initialisierung dieser neuen Elemente durch. Daher sind die alten Event-Listener bei nachgeladenen Elementen wirkungslos.

Wir müssen also dafür sorgen, dass die betreffenden Elemente wissen, dass es durchaus einen (oder auch mehrere) Event-Listener gibt. Dazu sollte dieser aber nicht direkt an das Element gehängt, sondern außerhalb von diesem platziert werden. Diesen Vorgang bezeichnet man als „delegaten“.

Um euch dies in der Praxis zu zeigen, hier mal ein Beispiel für einen regulären jQuery-Event-Listener (wird bei einem Klick ausgelöst):

$("#my_button").click(function(){
	alert('Click event triggered!');
});

Wie man hier sieht, wird zuerst das betreffende Element („#my_button„) ausgewählt, und dann auf dieses ein Klick-Event-Listner gelegt.

Bei einem delegateten Listener sieht dies nun ein wenig anders aus:

$("body").delegate("#my_button", "click", function(){
	alert('Click event triggered!');
});

In diesem Fall wählen wir als erstes statt dem Ziel-Element („#my_button„) ein beliebiges anderes aus, welches sich garantiert nicht ändern wird (hier „body„). Diesem sagen wir nun, dass es einen Event für ein anderes Element „halten“ wird.

Innerhalb der delegate()-Funktion geben wir nun an, welches Ziel-Element wir damit gerne ansprechen möchten („#my_button„) und auf welchen Auslöser es reagieren soll (hier: „click„).

Das bringt uns in der Praxis jenen Vorteil, dass wir Elemente beliebig austauschen und ersetzen können und die Event-Listener dennoch funktionieren, da sich diese ganz wo anders befinden.

 

Schritt 2: Verlinkungen über Javascript umleiten

Würde man auf der Website einen Link anklicken, würde der Browser diesen ganz normal laden, indem er einen kompletten Reload der Website (wenn es interner Link ist) durchführt.

Aber der Sinn von PJAX ist ja, genau dieses Verhalten zu unterbinden, und Inhalte dynamisch auszutauschen bzw. bei Bedarf nachzuladen.

Dazu legen wir auf den Link einen Klick-Event-Listener, welcher das Standardverhalten blockiert, sodass wir nun unseren eigenen Link-Handler definieren können:

$('body').delegate('a', 'click', function (e) {
	// prevent default behaviour of the link
	e.preventDefault();

	// TODO: handle custom link behaviour
});

Das Problem an diesem Event-Listener ist aber, dass dieser ALLE Links blockieren würde, aber unser eigener Link-Handler nur die internen Verlinkungen behandeln soll.

Ich hab euch dafür mal folgenden Event-Handler vorbereitet:

$('body').delegate('a', 'click', function (e) {
    var link = e.currentTarget;
    var isError = false;

    // Middle click, cmd click, and ctrl click should open links in a new tab as normal.
    if (e.which > 1 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
        isError = true;
    }

    // Ignore cross origin links
    if (location.protocol !== link.protocol || location.hostname !== link.hostname) {
        isError = true;
    }

    // Ignore case when a hash is being tacked on the current URL
    if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location)) {
        isError = true;
    }

    // Ignore event with default prevented
    if (e.isDefaultPrevented()) {
        isError = true;
    }

    // Check if link target is "_blank"
    if ($(this).attr('target') == '_blank') {
        isError = true;
    }


    // Perform link click
    if (!$(this).hasClass('dropdown-toggle') &&
        !$(this).data('toggle') &&
        !isError) {

        // We define the DOM element "main" as primary container for our replaced content
        var targetSelector = 'main';
        var $target = $(targetSelector);
        var href = $(this).attr('href');

        e.stopPropagation();
        e.preventDefault();

        if (!$target.length) {
            // target element does not exist
            return;
        }

        $.get(href, function (data, status) {
            if (status === 'success') {
                var parser = new DOMParser();
                var $data = $(parser.parseFromString(data, 'text/html'));
                var $newTarget = $data.find(targetSelector);
                var newTargetHtml = $newTarget.html();

                // Set HTML content from response
                $target.html( newTargetHtml );
            }
            else {
                alert("Unexpected response code: " + status);
            }
        });
    }
});

Als Erstes wird Schritt für Schritt überprüft, ob der betreffende Link von unserem Klick-Handler behandelt werden soll oder nicht.

Für den positiven Fall wird nun festgelegt, welcher Bereich gegen den neuen ausgetauscht werden soll – in diesem Fall wäre es der <main/>-Block innerhalb des HTML-Bodys (definiert in der Variable targetSelector).

Anschließend kann die eigentliche URL des Links via AJAX geladen werden (mittels $.get()).

Wenn die URL nun via AJAX geladen werden konnte, sagen wir jQuery explizit, dass die Antwort unter Nutzung des DOMParser() als HTML interpretiert werden soll.

In diesem geparsten HTML suchen wir nun unseren gewünschten Block (in diesem Fall <main/> mittels der Variable targetSelector von oben), und ersetzen diesen Block im alten HTML dann gegen den neuen, indem wir mittels der html()-Funktion einfach dessen Inhalt mit jenem von unserem geparsten und gefilterten HTML überschreiben.

 

Schritt 3: Formulare im Hintergrund versenden

Das gleiche Verhalten wie bei den Links übertragen wir nun auch auf Formulare.

Der einzige Unterschied hierbei ist jener, dass wir beim Absenden des Formulars im Prinzip nur einen kleinen Teilbereich ersetzen müssen.

Am einfachsten ist es hier einen Container um das eigentliche Formular herum zu legen.

<div id="my-form-container">
    <form action="" method="POST" data-refresh-target="#my-form-container">
            <input type="text" name="demo">
            
            <button type="submit">Send message</button>
    </form>
</div>

Beim Formular selbst legen wir nun mit Hilfe eines Data-Attributs (hier als „data-refresh-target“ bezeichnet) fest, in welchen Container wir die Antwort nach dem Absenden gerne laden möchten.

Nun aber zu unserem Submit-Handler:

$('body').delegate('form', 'submit', function (e) {
    var $this = $(this);
    var targetSelector = $this.data('refresh-target');
    var $target = $(targetSelector);

    if (!$target.length) {
        // target element does not exist
        return;
    }

    e.stopPropagation();
    e.preventDefault();


    var rqMethod = 'GET';
    var attrRqMethod = $(this).attr('method');
    var url = $this.attr('action');

    if (attrRqMethod) {
        rqMethod = attrRqMethod;
    }

    $.ajax({
        //mimeType: 'text/html; charset=utf-8', // ! Need set mimeType only when run from local file
        url: url,
        data: $(this).serialize(),
        type: rqMethod,
        cache: false,
        success: function (data, status, XHR) {

            // Set HTML content from response
            $target.html( data );

        },
        dataType: 'html'
    });
});

Innerhalb von diesem lesen wir nun mittels der data()-Funktion den Ziel-Container aus (über das Data-Attribut „refresh-target„, welches wir zuvor bereits am Form-Element festgelegt haben), in welchen wir die Antwort später hineinladen möchten.

Als nächstes prüfen wir, ob das betreffende Formular via GET oder via POST versendet werden soll. Wir bekommen diese Information direkt vom Form-Element anhand des Attributs „method„.
Ist dieses beispielsweise nicht definiert, wird ein Formular immer via GET versendet.

Damit auch der Inhalt der Formular-Felder übergeben wird, serialisieren wir diese mittels der serialize()-Funktion und übergeben dies als Parameter „data“ der ajax()-Funktion.

Anstatt der ajax()-Funktion könnte man ebenso gut mit $.get() oder $.post() arbeiten. Der Vorteil bei Ersterem liegt darin, dass wir den Request-Typ (GET oder POST) direkt über ein einzelnes Attribut („type„) steuern können.

Gleich wie beim Link-Handler von oben ersetzen wir nun einfach den Inhalt vom alten Container durch das zurückgelieferte HTML.
Wir gehen bei diesem Beispiel davon aus, dass als Antwort nur jenes HTML zurückkommt, welches 1:1 in den Container eingefügt wird, also ohne das restliche Layout rundherum.

 

Vollständiges Beispiel (Demo)

Damit wären wir nun mit allen erforderlichen Maßnahmen für das asynchrone Datenhandling durch.

Da das Ganze aber am Anfang doch etwas kompliziert ist, hab ich euch ein kleines Beispiel (basierend auf dem CodeIgniter-Framework) zusammengestellt:

Demo herunterladen

Kopiert das Ganze auf einen Webserver (z.B. XAMPP, LAMP, etc.) und schon könnt ihr euch ein bisschen durch den Dummy durchklicken.

Die Scripts in  diesem Beispiel sind zum Teil bereits etwas erweitert – beispielsweise gibt es hier einen Fading-Effekt beim Ersetzen des Inhalts, damit das Ganze noch ruhiger wirkt.

Keine Sorge – es ist alles durchgehend kommentiert. Ihr solltet euch da also ohne große Probleme zurechtfinden können.

 

Fazit

PJAX dient in erster Linie dazu, das Erlebnis für den Besucher angenehmer zu gestalten, indem dieser nicht von permanenten Seiten-Reloads abgelenkt wird.

Aufgrund der Tatsache, dass auch externe Ressourcen nur einmal initial geladen werden müssen, fällt dies bei weiteren Interaktionen weg, was sich in weiterer Folge positiv auf die Ladezeit und damit verbunden auf die Performance generell auswirkt.

Man kann die PJAX-Technik übrigens noch weiter ausreizen, indem man beispielsweise als Header-Variable oder zusätzliche GET-/POST-Variable mitsendet, ob man das Layout rund um das eigentliche HTML haben möchte oder nicht.
Serverseitig könnte man dann in weiterer Folge dynamisch entscheiden, ob man das gesamte Markup rendert, oder eben nur einen Teilbereich davon.

Im oberen Beispiel hab ich euch das bereits auf eine stark vereinfachte Art und Weise gezeigt – normale Links laden jeweils das gesamte Markup inkl. Layout, bei den Formularen hingegen wirklich nur das betreffende HTML.

Es gibt zahlreiche Möglichkeiten, wie man die Performance einer Website steigern kann – dies hier ist aber nur eine von vielen.