Javascript und der </body>-Tag

„Kaum macht man es richtig, schon funktioniert es.“

Ein simples Sprichwort – doch kann man das Ganze bei Javascript umdrehen:

„Es funktioniert, also ist es richtig?“

Womit ich beim Thema wäre :-)

Paul Lewis und Jake Archibald haben mich mit ihrem Videoblog HTTP 203 zum Grübeln gebracht. Als erfahrene JS Entwickler hängen wir unseren Code erst vor dem </body>-Tag ein und führen ihn frühestens im document.ready Event aus…

Und um ihr Beispiel (von mir frei übersetzt) zu zitieren:

„Das ist so, als würde man sicher stellen dass die Straßen bis 8:45h frei sind und dann fahren alle auf einmal los“ (HTTP 203: Performance Matters ~30s)

Die Idee ist dass alles nicht zwingend zum document.ready Zeitpunkt ausgeführt werden muss, sondern pro Situation verschiedene Szenarien möglich sind, sei es im <head>, zum document.ready, window.ready oder zu einem ganz anderen Zeitpunkt. (whaaaaat??!!)

Das Beispiel ist so einleuchtend, dass man sich direkt schämt nicht selbst draufgekommen zu sein. Der Browser hat es gerade geschafft den DOM und das CSS zu parsen, da versucht man ihn mit jQuery, Plugins, einem MVC und seinem eigenen Code schachmatt zu setzen.

Viele JS Dinge benötigen einen fertigen DOM, und generell blockiert jedes Javascript erstmal den Browser bis dieser das CSSOM fertiggestellt hat (Kritischer Rendering Pfad) – es sei denn man setzt das async Attribut auf dem <script>-Tag.

Als Beispiel dient die Initialisierung eines scroll Listeners bzw. die Initialisierung einer Schnittstelle für einen high performance scroll Handler.

Entwickler A schreibt zum Beispiel folgendes:

$(window).on('scroll', function myParallaxHandling(e) { … });

und Entwickler B an einer anderen Stelle das gleiche mit einer anderen Funktion und im besten Fall gehen beide auf Nummer sicher und schreiben zu guter Letzt $(window).trigger('scroll'); um ihren Code einmal auszuführen.

Das das kein guter Stil ist, ist leider nicht Thema dieses Postings – aber der Vollständigkeit halber: Wenn solche Handler einmal ausgeführt werden müssen, dann kapselt man sie in einer eigenen Funktion und ruft nur diese Funktion einmal auf – diese kann dann entweder mit einem undefined Parameter umgehen, oder man bastelt ein Objekt dass erstmal alles mitbringt. Da man nicht weiß wer noch alles auf das scroll Event hört, kann sich ein trigger('scroll') als echter Showstopper herausstellen.

Die Idee ist, einen performanten Scroll Listener direkt zu initialisieren, der ein Custom Event wirft, an dass sich alle anderen Entwickler hängen können:

(function (w, u) {
  'use strict';
  var prefix = 'hp',
      rtPrefix = 'rt',
      endPrefix = 'end',
      delay = 133,
      endDelay = 231,
      delayTimeout,
      delayEvent,
      realtimeTimeout,
      realtimeEvent,
      endScrollTimeout,
      endScrollEvent;

  // for IE11! and older support make sure to use this polyfill:
  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent

  function requestAnimationFrame(cb) { 
    // for REAL browser support include this polyfill:
    // https://gist.github.com/paulirish/1579671
    if (!!w.requestAnimationFrame) {
      return w.requestAnimationFrame(cb); 
    } else {
      return w.setTimeout(cb, 16);
    }
  }

  function handleDelayedScrolling() {
    var hpEvent = new CustomEvent(
          [prefix, delayEvent.type].join('-'),
          {'origin': delayEvent}
    );
    w.clearTimeout(delayTimeout);
    delayTimeout = u;
    w.dispatchEvent(hpEvent);
  }

  function handleRealtimeScrolling() { 
    var rtEvent = new CustomEvent(
          [prefix, rtPrefix, realtimeEvent.type].join('-'),
          {'origin': realtimeEvent}
    );
    if (!!w.cancelAnimationFrame) {
      w.cancelAnimationFrame(realtimeTimeout);
    } else {
      w.clearTimeout(realtimeTimeout);
    }
    realtimeTimeout = u;
    w.dispatchEvent(rtEvent);
  }

  function handleEndScroll() {
    var endEvent = new CustomEvent(
          [prefix, endPrefix, endScrollEvent.type].join('-'),
          {'origin': endScrollEvent}
    );

    w.clearTimeout(endScrollTimeout);
    endScrollTimeout = u;
    w.dispatchEvent(endEvent);
  }

  function handleScrolling(e) { 
    if (!realtimeTimeout) {
      realtimeTimeout = requestAnimationFrame(handleRealtimeScrolling);
    }
    realtimeEvent = e;

    if (!delayTimeout) {
      delayTimeout = w.setTimeout(handleDelayedScrolling, delay);
    }
    delayEvent = e;

    w.clearTimeout(endScrollTimeout);
    endScrollTimeout = w.setTimeout(handleEndScroll, endDelay);
    endScrollEvent = e;
  }

  w.addEventListener('scroll', handleScrolling);
}(window));

Im Klartext 2.414 Bytes, minifiziert ~726 Bytes.

Dieses Script kann gefahrlos im <head> geladen werden, da keinerlei DOM-Manipulation veranstaltet werden die den Browser im Renderflow behindern, und bietet drei verschiedene Scroll Events (hp = highperformance, rt = realtime):

  1. $(window).on('hp-rt-scroll', function (e) { console.log(e.origin); }); Dieser Callback wird minimal verzögert ausgeführt, nämlich bis der nächste AnimationsFrame vom Browser erreicht wird. Spätestens nach 1/60 Sekunde, also 16.6667 ms – daher der Begriff realtime.
  2. $(window).on('hp-scroll', function (e) { console.log(e.origin); }); Dieser Callback wird etwas verzögert ausgeführt, nämlich alle 133 ms während eines Scrollvorgangs. Ein Parallax Effekt dürfte hiermit trotzdem gut realisierbar sein.
  3. $(window).on('hp-end-scroll', function (e) { console.log(e.origin); }); Dieser Callback wird nur einmal per Scroll-Vorgang ausgeführt, nämlich wenn für 231 ms kein Scroll Event mehr gefeuert wurde. Wenn man zum Beispiel einen „To-Top“ Button ein/ausblenden will, könnte man diese Logik hier implementieren.

Um meine Theorie zu untermauern habe ich die Startseite meines Blogs genutzt und dort 10x die Startseite ohne Cache geladen und gemessen, 10x mit diesem Script vor dem </body> und 10x mit diesem Script vorm </head>:

  • keine Änderung: DOMContentLoaded: 2.108s; Load: 2.384s
  • vor </body>: DOMContentLoaded: 1.93s; Load: 2.234s
  • vor </head>: DOMContentLoaded: 1.942; Load: 2.23s

Diese Messung ist aber nicht aussagekräftig, ich muss den Blog auf meinem Laptop spiegeln und dort nochmal messen, damit Internet Latenzen ausgemerzt werden, denn die Messwerte hatten eine ziemliche Schwankung. to be continued…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.