INDIVIRTUAL - TECHNISCH PARTNER IN DIGITALE DIENSTVERLENING

The Virtual DOM

April 17, 2019

The Virtual DOM

Om een goed beeld te krijgen van wat een Virtual DOM is, is het goed het concept van de DOM te begrijpen. Dus eerst wil ik ingaan op wat een DOM eigenlijk is en waarom het manipuleren ervan niet optimaal is.
Dan is het tijd voor de Virtual DOM als concept, en hoe deze is geïmplementeerd in React en het daarbijbehorende Diffing/Reconciliation algorithme.
Als klap op de vuurpijl geef ik een paar snelle tips om je React code te optimaliseren voor de Virtual DOM.

Document Object Model

Het Document Object Model (afgekort tot DOM) is een objectgeoriënteerde benadering van gestructureerde documenten zoals HTML-, XHTML- en XML-documenten.

virtual-dom

Elke node in de boom representeert een (HTML) element binnen dat document, en kan connectie maken met JavaScript zodat er interactie kan ontstaan door middel van events (wat vroeger werd aangeduid met DHTML).

Een bekend probleem van de DOM is het aanpassen of manipuleren ervan. Een node aanwijzen en aanpassen met JavaScript verloopt totaal niet efficiënt. Dit omdat nádat de rendering engine de HTML heeft geparsed; en de daarbijbehorende CSS heeft toegevoegd; de DOM gepresenteerd wordt als een soort abstractie.

Elke aanpassing in de DOM veroorzaakt een nieuwe queue in de rendering engine: er wordt opnieuw HTML geparsed, gerenderd en de CSS engine triggered een re-paint. Dit is verre van optimaal, want dat zijn nogal wat handelingen.

virtual-dom-DOM

Dit betekent dat als je 40 nodes gaat aanpassen, de render engine vertraagt omdat het hele re-render process geldt voor elke aanpassing.
Precies dit probleem stelt een Virtual DOM op te lossen.

Virtual DOM

In plaats van elke aanpassing rechtstreeks in de DOM te renderen, worden ze eerst toegepast op een Virtuele representatie daarvan.
We hebben het nu dus over een abstractie van een abstractie. Of een Document Object Model Model… 👻
Je kunt een Virtual DOM zien als een Mock van de echte DOM, welke efficiënte algoritmes gebruikt om DOM mutaties te berekenen.

Dit klinkt geavanceerder dan wat het in werkelijkheid is. Het is niets meer als een JavaScript object waarin de DOM structuur wordt vertegenwoordigd, en die wordt bijgehouden in het geheugen (van bijvoorbeeld een browser).
Hoewel dit principe door React is aangebracht, maken populaire frameworks zoals Vue en Ember tegenwoordig ook gebruik van een soortgelijke Virtual DOM.

React Renderer

Binnen een render cycle van React, met of zonder JSX, wordt de template compiler verteld hoe de Virtual DOM eruit komt te zien. Hiervoor wordt een nieuwe Virtual DOM aangemaakt, en er ontstaan dus 2 Virtual DOMs.
Vervolgens worden deze twee vergeleken en wordt er gekeken of er in slimme batches (of het liefst zelfs één patch) gewerkt kan worden om de echte DOM te updaten.

Dit klinkt misschien erg overdreven, maar omdat een Virtual DOM niets meer is als een plat object bestaande uit React objecten kan de boel snel gaan. Binnen WebKit zelfs zot 200.000 mutaties op objecten per seconde.

Interessant om te vermelden is dat het principe van een Virtual DOM binnen React niet strikt gaat om het updaten van HTML DOM. Met bijvoorbeeld React Native wordt een Virtual DOM vertaald naar iOS en Android views, met dezelfde optimalisaties op het oog.

Reconciliation

React maakt gebruik van een vergelijkings algoritme, met een fancy woord: Reconciliation, wat een soort diffing inhoudt.
Ik wil er hier niet te diep op ingaan want in de documentatie van React wordt dat al voldoende gedaan. Maar puntje bij paaltje is dat React gebruik maakt van dit algoritme om het minimum aan mutaties te vinden die nodig zijn om de echte DOM te updaten. Uitgangspunt is om voor elke lifecycle van de hele applicatie één repaint te triggeren, in de vorm van een patch.

React begint daarmee bovenaan de component tree, en als daar al een aanpassing heeft plaatsgevonden, zal de hele tree aangemerkt worden als dirty, verwijderd worden en van scratch geüpdatet worden.

virtual-dom-repaint

Kortom, een parent component zorgt voor een re-render van zichzelf inclusief alle child-components.

Dit klinkt niet heel efficiënt, maar het is in ieder geval vele malen efficiënter als blind wegschrijven.
Daarnaast kan een developer verschillende technieken toepassen om de Virtual DOM te helpen het Reconciliation process te verbeteren.

Optimalisaties en best practises

Als developers kunnen we de Virtual DOM, en daarmee de snelheid van je applicatie, enorm helpen. React biedt daarvoor verschillende tools en handvatten.

Keys
Een belangrijk, maar toch vaak over het hoofd gezien aspect binnen React components zijn zogenaamde keys.
React keys zijn simpele attributen voor je Components, die een enorme impact kunnen hebben op de performance van je app.

Een simpel voorbeeld, wat de meeste React developers meteen zullen herkennen:
virtual-dom-keys

In de render method van je component loop je alle personen naar een eigen sub-component. Als key wordt hier de index van de loop gebruikt, wat in principe werkt, maar eigenlijk een anti-pattern is.
React gebruikt deze keys om in de Virtual DOM Reconciliation toe te passen.
Het probleem hier is dit: Mocht ik, hoe dan ook, een item vooraan de array van personen plaatsen, schuift de index per item op. Dat betekent dat alle items herbouwd zullen worden, want React is de referentie kwijt.
Wat developers zouden moeten doen, is een echt uniek, stabiel id meegeven aan het key attribute zodat React optimaal z’n Reconciliation Algorithm kan toepassen en voorkom je onnodige berekeningen.

Dit is een klein voorbeeld, maar als elk component voor een persoon zou bestaan uit meerdere andere componenten dan wordt dit antipattern een dure berekening binnen een gigantische tree.

shouldComponentUpdate || Purecomponent
React streeft ernaar dat componenten zoveel mogelijk als functional components worden geschreven. Toch is er vaak binnen je App architectuur een High Order Component aanwezig in de vorm van een class Component.
Om binnen een class Component invloed uit te oefenen op of er wel of niet gerenderd moet worden, bestaat er binnen de cifecycle methods en method genaamd shouldComponentUpdate. Binnen deze method kun je de vorige state en props vergelijken met de nieuwe, en uiteindelijk een boolean teruggeven. Niet alle state en props hoeven van invloed te zijn op het huidige component.

Als je wel gebruik maakt van de lifecycle, maar niet zit te wachten op een re-render van je Component omdat je geen props of state verwacht, zorg dan dat je class de Purecomponent inherit van React. Purecomponent werkt hetzelfde als de standaard Component van React, alleen tijdens shouldComponentUpdate wordt een zogenaamde shallow comparison gedaan, en zal de het render beperkt worden out of the box.

memo()
Voor het optimaliseren van renderende functional components is er sinds React 16.6 ook een oplossing. Men kan aangeven dat op een functional component memoizing toegepast kan worden, door de functie te wrappen in memo().
memo() werkt hetzelfde als PureComponent voor class components.

virtual-dom-memo

React Fiber

Met de release van React 16, Fiber genaamd, heeft het team van React het Reconciliation gedeelte totaal herschreven. De grootste aanpassing zat ‘m in het traversen van de DOM. In plaats van recursive top-bottom te traversen, is er een nieuwe data-structuur aan de Virtuele DOM toegevoegd, ook wel een Fiber genoemd.

Ook dit principe is simpeler als het klinkt. Een Fiber is niets minder als een JavaScript object dat de relatie tussen componenten (parent - children) bijhoudt, volgens het Singly Linked List pattern.

Als je dit naast een Virtuele DOM houdt, kan snel berekend worden welke updates gegroepeerd kunnen worden voor een patch.
Dit versneld het renderproces aanzienlijk, zeker voor het asynchroon laden van componenten, die je op hun beurt weer kan prioriteren met lazy(), wat naast memo() is geïntroduceerd met React 16.6.

Conclusie

Met het omschrijven van de React Reconciliation core naar React Fiber is er enorm veel verbeterd als het gaat om performance. React projecten worden steeds omvangrijker, en React wordt nu zowel client-side als server-side toegepast. Daar hoort een geoptimaliseerde core bij.

Al met al, React biedt een breed framework aan, waarmee geoptimaliseerde professionele applicaties gemaakt kunnen worden. En het ziet er naar uit dat React voorlopig blijft.

Als laatste tip, voor degenen die React performance willen testen, er is (natuurlijk) een library beschikbaar (meerdere) waarmee je je eigen code kan testen op performance.

Ik hoop dat het helpt!

Tom Cool

Tom Cool

Front-end Developer