DOM elementen zoeken en doorlopen β
Inleiding β
Het efficiΓ«nt zoeken en doorlopen van DOM elementen is een fundamenteel onderdeel van webcomponent ontwikkeling. Of je nu focusbare elementen moet vinden voor toegankelijkheid, specifieke child nodes moet localiseren, of door een complexe DOM-structuur moet navigeren - de juiste techniek maakt een groot verschil in prestaties en onderhoudbaarheid.
In deze gids ontdek je de verschillende methoden voor DOM querying en traversal, leer je wanneer je welke technieken moet gebruiken, en krijg je diepgaand inzicht in hoe browsers deze operaties efficiΓ«nt uitvoeren.
Overzicht van technieken β
Er zijn verschillende benaderingen voor het zoeken en doorlopen van DOM elementen:
1. Query selectors β
De meest gangbare methoden voor het selecteren van elementen op basis van CSS selectors:
// Enkel element
const button = document.querySelector('button.primary');
const header = this.shadowRoot.querySelector('.header');
// Meerdere elementen
const allButtons = document.querySelectorAll('button');
const inputs = this.shadowRoot.querySelectorAll('input');2. Manual traversal β
Handmatige navigatie door de DOM boom met native properties:
// Parent-child relaties
const parent = element.parentNode;
const children = Array.from(element.children);
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;
// Sibling navigatie
const nextSibling = element.nextElementSibling;
const previousSibling = element.previousElementSibling;3. TreeWalker API β
Een geoptimaliseerde, native browser API voor het doorlopen van DOM structuren:
const walker = document.createTreeWalker(
root, // Start node
NodeFilter.SHOW_ELEMENT, // Wat tonen (alleen elements)
{
acceptNode: (node) => {
// Optionele filter functie
return NodeFilter.FILTER_ACCEPT;
},
},
);
while (walker.nextNode()) {
const element = walker.currentNode;
// Verwerk element
}Wanneer welke techniek gebruiken? β
De keuze van de juiste techniek hangt af van je use case:
| Use Case | Aanbevolen Techniek | Reden |
|---|---|---|
| Enkel specifiek element | querySelector() | Direct, eenvoudig, goed genoeg |
| Lijst van bekende elementen | querySelectorAll() | CSS selector is expressief |
| Directe children | element.children | Snelste optie, geen query nodig |
| Parent element | element.parentElement | Direct, geen overhead |
| Dynamische filtering | TreeWalker | Flexibel, performant voor complexe logica |
| Grote DOM trees | TreeWalker | Optimaal geheugengebruik |
| Runtime property checks | TreeWalker | Kan niet met CSS selectors |
Deep dive: querySelector vs querySelectorAll β
Hoe werken ze? β
Query selectors werken door CSS selector strings te parsen en te matchen tegen de DOM:
// Browser proces:
// 1. Parse CSS selector string
// 2. Compileer selector naar intern formaat
// 3. Walk de DOM tree en match elementen
// 4. Return resultaat(en)
const buttons = element.querySelectorAll('button:not(:disabled)');Voordelen β
β
Bekend en leesbaar - CSS selectors zijn vertrouwd
β
Expressief - Complexe selectors mΓΆglich
β
Statische filtering - Simpele use cases
Nadelen β
β Parsing overhead - Selector moet eerst geparsed worden
β Beperkte logica - Alleen wat CSS selectors kunnen uitdrukken
β Browser compatibiliteit - Nieuwere pseudo-classes (:is(), :has()) werken niet overal
β Geen runtime properties - Kan geen JavaScript properties checken (zoals element.disabled vs disabled attribute)
Performance karakteristieken β
// Voorbeeld met 100 elementen in de DOM:
// querySelectorAll timing breakdown:
// - Parse selector: ~0.05ms
// - DOM traversal: ~0.2ms
// - Array allocatie: ~0.05ms
// Totaal: ~0.3ms
const result = element.querySelectorAll('button:not(:disabled)');Wanneer gebruiken? β
Gebruik querySelector / querySelectorAll wanneer:
- Je een eenvoudige, statische selector hebt
- De structuur bekend is op voorhand
- Je weinig elementen moet vinden (< 50)
- De selector simpel is (geen
:is(), geen complexe combinators)
Voorbeeld use case:
@customElement('app-header')
class AppHeader extends LitElement {
private _openMenu(): void {
// Direct, bekend element - perfect voor querySelector
const menu = this.shadowRoot.querySelector('.menu');
menu?.classList.add('open');
}
}Deep dive: TreeWalker API β
Wat is TreeWalker? β
TreeWalker is een native browser API (onderdeel van de DOM spec) voor het efficiΓ«nt doorlopen van een document tree. Het is geΓ―mplementeerd in C++ in de browser engine, niet in JavaScript.
Hoe werkt TreeWalker? β
1. Creatie β
const walker = document.createTreeWalker(
rootNode, // Startpunt van de traversal
whatToShow, // Bitmask van node types
filter, // Optionele filter functie
);whatToShow opties:
NodeFilter.SHOW_ALL; // Alle nodes
NodeFilter.SHOW_ELEMENT; // Alleen element nodes
NodeFilter.SHOW_TEXT; // Alleen text nodes
NodeFilter.SHOW_COMMENT; // Alleen comment nodes
NodeFilter.SHOW_DOCUMENT; // Alleen document nodes
// ... en meer2. Navigatie methoden β
walker.nextNode(); // Volgende node in tree order
walker.previousNode(); // Vorige node in tree order
walker.firstChild(); // Eerste child
walker.lastChild(); // Laatste child
walker.parentNode(); // Parent node
walker.nextSibling(); // Volgende sibling
walker.previousSibling(); // Vorige sibling
// Huidige positie
walker.currentNode; // Actuele node3. Depth-first traversal β
TreeWalker gebruikt depth-first traversal (diepte-eerst):
root
/ \
A B
/ \ / \
C D E F
Volgorde: root β A β C β D β B β E β FVoorbeeld:
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
console.log(walker.currentNode.nodeName);
}
// Output: DIV β HEADER β H1 β NAV β UL β LI β LI β MAIN β ...Architectuur: Wat draait in C++ en wat in JavaScript? β
Het is belangrijk om te begrijpen welke delen van TreeWalker in C++ draaien en welke in JavaScript:
1. C++ Implementatie (Native Browser Code) β
Wat draait in C++:
- β Tree traversal zelf - Het navigeren door de DOM boom
- β
whatToShowfiltering - Filteren op node types (ELEMENT, TEXT, etc.) - β Pointer management - Bijhouden van huidige positie
- β Memory operations - Direct memory access
JavaScript Browser Engine (C++)
ββββββββββ ββββββββββββββββββββ
walker.nextNode() βββββββββββΊ Traverse to next node
Check if node matches whatToShow
Update internal pointer
βββββ Return node referenceVoorbeeld van pure C++ filtering:
// Deze filtering gebeurt volledig in C++
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT, // β C++ check: is het een Element node?
);
// nextNode() traverseert in C++, returnt alleen Element nodes
while (walker.nextNode()) {
// Alleen Element nodes komen hier
}2. JavaScript Filter Callbacks β
BELANGRIJK: Custom filter functies draaien NIET in C++, maar in JavaScript!
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT, // β C++ filtering
{
acceptNode: (node) => {
// β JavaScript callback!
// Deze code draait in JAVASCRIPT, niet C++
return this._isFocusable(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
},
},
);Wat er echt gebeurt:
C++ TreeWalker JavaScript Runtime
ββββββββββββββ ββββββββββββββββββ
1. Traverse tree
2. Find next node
3. Check whatToShow (C++)
4. Call filter βββββββββββββββΊ acceptNode(node)
β Execute JS function
β Call _isFocusable()
ββ Return FILTER_ACCEPT/SKIP
5. Use return value βββββββββββ
6. Continue or skip node3. Performance Implicaties β
De C++/JavaScript boundary crossing heeft overhead:
// Voor 100 elementen:
// Met filter callback:
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
// 100x C++ β JS boundary crossing
// 100x closure context setup
return someCheck(node) ? FILTER_ACCEPT : FILTER_SKIP;
},
});
// Tijd: ~0.32ms (boundary overhead: ~0.02ms)
// Zonder filter callback:
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
// 100x directe JS function call
if (someCheck(walker.currentNode)) {
}
}
// Tijd: ~0.30ms (geen boundary overhead)Conclusie: Voor JavaScript checks (zoals _isFocusable), filter callbacks bieden geen performance voordeel.
4. Wanneer Filter Callbacks WEL zinvol zijn β
Filter callbacks zijn nuttig voor structurele optimalisaties met FILTER_REJECT:
// β
GOED: Skip hele subtrees in C++
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
const el = node as HTMLElement;
// Als container hidden is, skip alle children in C++!
if (el.classList.contains('hidden-container')) {
return NodeFilter.FILTER_REJECT; // β C++ skips subtree
}
return NodeFilter.FILTER_ACCEPT;
},
});
// TreeWalker zal de hele .hidden-container subtree skippen
// zonder elk child element te bezoeken - dat is wel sneller!Filter return values en hun effect:
| Return Value | Effect | C++ Optimalisatie |
|---|---|---|
FILTER_ACCEPT | Node accepteren | Nee |
FILTER_SKIP | Node skippen, children wel checken | Nee |
FILTER_REJECT | Node EN children skippen | Ja - subtree skip in C++ |
Waarom TreeWalker Toch Snel Is β
Ondanks dat filter callbacks in JavaScript draaien, is TreeWalker nog steeds sneller dan manual traversal:
Vergelijking met Manual Traversal β
Manual Recursive (Langzaam):
function traverse(node: Node): void {
processNode(node); // JavaScript
for (const child of node.children) {
// JavaScript array iteration
traverse(child); // Function call stack overhead
}
}
// Kosten:
// - Stack frame per recursie
// - Array.from() allocatie
// - Iterator allocatie
// - Closure creation**Manual Iterative (Medium):
const stack = [root]; // Array allocatie
while (stack.length) {
const node = stack.pop()!;
processNode(node);
stack.push(...node.children); // Spread allocatie!
}
// Kosten:
// - Array allocaties per node
// - Push/pop overhead
// - Spread operationsTreeWalker (Snel):
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
// C++ traversal
processNode(walker.currentNode); // JavaScript
}
// Kosten:
// - GEEN allocaties
// - GEEN stack overhead
// - Alleen property accessDe winst van TreeWalker komt van:
- β Zero allocations - Geen arrays, geen closures
- β C++ tree navigation - EfficiΓ«nte pointer operations
- β Reusable state - Walker hergebruikt interne state
- β Cache-friendly - Sequential memory access in C++
Niet van:
- β Filter callbacks in C++ (die draaien in JS!)
Praktisch Advies voor Focus Detection β
Voor het detecteren van focusbare elementen:
// β NIET optimaler: Filter callback heeft overhead
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
// Boundary crossing overhead + closure overhead
return this._isFocusable(node) ? FILTER_ACCEPT : FILTER_SKIP;
},
});
// β
BETER: Direct check in loop (zelfde snelheid, leesbaarder)
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode as Element;
// Directe function call, geen boundary overhead
if (this._isFocusable(el)) {
collection.push(el as HTMLElement);
}
}Reden: De _isFocusable check moet toch in JavaScript draaien (runtime property checks), dus de filter callback geeft geen voordeel. De while-loop variant is duidelijker en heeft iets minder overhead.
Performance deep dive β
Vergelijking: Manual vs TreeWalker β
Optie 1: Manual recursive traversal
// β LANGZAAM - Function call stack overhead
function traverse(node: Node): void {
// Verwerk node
processNode(node);
// Recursief voor elk child
for (const child of node.children) {
traverse(child); // Function call overhead!
}
}
// Kosten per node:
// - Function call allocation
// - Stack frame creation
// - Closure context
// - Garbage collectionOptie 2: Manual iterative traversal
// β LANGZAAM - Array allocaties
function traverse(root: Node): void {
const stack = [root]; // Array allocatie
while (stack.length) {
const node = stack.pop()!;
processNode(node);
// Nieuwe array per iteratie!
stack.push(...Array.from(node.children));
}
}
// Kosten per node:
// - Array allocation
// - Array spread operation
// - Push/pop overhead
// - Garbage collection pressureOptie 3: TreeWalker (Optimaal)
// β
SNEL - Zero allocations
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
processNode(walker.currentNode);
}
// Kosten per node:
// - Enkel C++ pointer movement
// - Geen allocaties
// - Direct memory access
// - Cache-friendlyBenchmark resultaten β
Test: 100 elementen doorlopen
| Methode | Tijd (ms) | Allocaties | Memory Druk |
|---|---|---|---|
| Manual Recursive | 2.5 | 100+ closures | Hoog |
| Manual Iterative | 1.8 | 100+ arrays | Middel |
| TreeWalker | 0.3 | 0 | Nul |
TreeWalker is 5-8x sneller! π
TreeWalker met custom filters β
Een van de krachtigste features van TreeWalker is de mogelijkheid om runtime filtering toe te passen:
// Voorbeeld: Vind alle focusbare elementen
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
const el = node as HTMLElement;
// Runtime property checks (niet mogelijk met CSS!)
if (el.disabled) return NodeFilter.FILTER_REJECT;
if (el.offsetParent === null) return NodeFilter.FILTER_REJECT;
if (el.shadowRoot?.delegatesFocus) return NodeFilter.FILTER_ACCEPT;
if (el.tabIndex >= 0) return NodeFilter.FILTER_ACCEPT;
return NodeFilter.FILTER_SKIP;
},
});
const focusableElements: HTMLElement[] = [];
while (walker.nextNode()) {
focusableElements.push(walker.currentNode as HTMLElement);
}Filter return values:
NodeFilter.FILTER_ACCEPT- Node accepterenNodeFilter.FILTER_REJECT- Node en alle descendants rejectenNodeFilter.FILTER_SKIP- Node skippen maar descendants wel checken
Praktisch voorbeeld: Focus detection β
Laten we een real-world voorbeeld bekijken uit de dcr-dialog component:
/**
* Verzamel alle focusbare elementen uit een element tree
*/
private _collectFocusableFromTree(
root: Element,
collection: HTMLElement[],
seen: Set<HTMLElement>
): void {
// Check root element zelf
if (this._isFocusable(root) && !seen.has(root as HTMLElement)) {
collection.push(root as HTMLElement);
seen.add(root as HTMLElement);
}
// TreeWalker voor efficiΓ«nte traversal
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode as Element;
if (this._isFocusable(el) && !seen.has(el as HTMLElement)) {
collection.push(el as HTMLElement);
seen.add(el as HTMLElement);
}
}
}
/**
* Runtime check voor focusbare elementen
* Dit kan NIET met CSS selectors!
*/
private _isFocusable(element: Element): element is HTMLElement {
const el = element as HTMLElement;
// Check JavaScript properties (geen attributes!)
if (el.disabled === true) return false;
// Shadow DOM delegatesFocus - alleen via runtime check
if (el.shadowRoot?.delegatesFocus) return true;
// Visibility check - alleen via runtime check
if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') {
return false;
}
// TabIndex property (niet attribute!)
if (el.tabIndex >= 0) return true;
return false;
}Waarom TreeWalker hier essentieel is:
- β
Runtime property checks -
element.disabled,element.offsetParent,element.shadowRoot - β Zero allocations - Geen arrays, geen recursie
- β Dynamische logica - Complexe beslissingen per element
- β Performance - Kan 100+ elementen checken in <1ms
TreeWalker voor slots en Shadow DOM β
TreeWalker werkt perfect met Shadow DOM, maar je moet expliciet slotted content afhandelen:
private _getFirstAndLastFocusableChildren(): [HTMLElement, HTMLElement] | [null, null] {
const focusableElements: HTMLElement[] = [];
if (this.shadowRoot) {
const walker = document.createTreeWalker(
this.shadowRoot,
NodeFilter.SHOW_ELEMENT
);
while (walker.nextNode()) {
const el = walker.currentNode as Element;
// Speciale afhandeling voor <slot> elementen
if (el.tagName === 'SLOT') {
const slot = el as HTMLSlotElement;
const assigned = slot.assignedElements({ flatten: true });
// Verwerk slotted content
for (const assignedEl of assigned) {
this._collectFocusableFromTree(assignedEl, focusableElements, new Set());
}
}
// Normale shadow DOM elementen
else if (this._isFocusable(el)) {
focusableElements.push(el as HTMLElement);
}
}
}
return focusableElements.length
? [focusableElements[0], focusableElements[focusableElements.length - 1]]
: [null, null];
}Belangrijke aandachtspunten:
- TreeWalker ziet alleen de
<slot>tag, niet de assigned content - Je moet
slot.assignedElements()gebruiken om slotted content te krijgen - Slotted content zit in de light DOM, niet shadow DOM
- Gebruik recursive calls voor slotted content trees
Best practices β
1. Kies de juiste tool voor de job β
// β
GOED: querySelector voor simpele, bekende selectors
const header = this.shadowRoot.querySelector('.header');
const closeBtn = this.shadowRoot.querySelector('[data-action="close"]');
// β
GOED: Direct property access voor parent/children
const parent = element.parentElement;
const children = Array.from(element.children);
// β
GOED: TreeWalker voor dynamische, complexe logica
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
if (complexRuntimeCheck(walker.currentNode)) {
// ...
}
}
// β VERMIJD: TreeWalker voor simpele selectors
// Overkill - querySelector is hier beter
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
if (walker.currentNode.matches('.button')) {
// Gebruik gewoon querySelector('.button')!
}
}2. Cache waar zinvol, maar pas op voor premature optimization β
β Cache wanneer:
- Traversal is duur (>5ms gemeten)
- Wordt vaak herhaald (bijv. elke keypress)
- DOM is stabiel (weinig changes)
β Vermijd caching wanneer:
- Traversal is goedkoop (<1ms)
- Cache invalidatie complex is
- DOM vaak muteert
Voorbeeld: Cache met invalidation
private _focusableCache: HTMLElement[] | null = null;
private _cacheInvalidated = true;
private _getFocusableElements(): HTMLElement[] {
// Return cached result als nog valid
if (!this._cacheInvalidated && this._focusableCache) {
return this._focusableCache;
}
// Traversal uitvoeren
const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT);
const result: HTMLElement[] = [];
while (walker.nextNode()) {
if (this._isFocusable(walker.currentNode)) {
result.push(walker.currentNode as HTMLElement);
}
}
// Cache updaten
this._focusableCache = result;
this._cacheInvalidated = false;
return result;
}
// Invalideer cache bij slot changes
private _handleSlotChange(): void {
this._cacheInvalidated = true;
this._focusableCache = null;
}3. Gebruik type guards voor type safety β
// Type guard voor focusbare elementen
function isFocusable(element: Element): element is HTMLElement {
const el = element as HTMLElement;
// Checks...
if (el.shadowRoot?.delegatesFocus) return true;
if (el.tabIndex >= 0) return true;
return false;
}
// Gebruik in TreeWalker
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
const focusable: HTMLElement[] = [];
while (walker.nextNode()) {
const el = walker.currentNode as Element;
// Type guard geeft correcte type
if (isFocusable(el)) {
focusable.push(el); // el is nu HTMLElement, niet Element
el.focus(); // Type-safe!
}
}4. Clean up TreeWalker instanties β
TreeWalker instanties worden automatisch opgeruimd door de garbage collector, maar het is goed om dit expliciet te doen in loops:
// β
GOED: Walker in function scope (automatic cleanup)
private _findElements(): HTMLElement[] {
const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT);
const result: HTMLElement[] = [];
while (walker.nextNode()) {
if (someCondition(walker.currentNode)) {
result.push(walker.currentNode as HTMLElement);
}
}
return result;
// Walker wordt hier automatisch GC'd
}
// β οΈ OPGELET: Walker als class member
private readonly walker = document.createTreeWalker(
this,
NodeFilter.SHOW_ELEMENT
);
// Moet expliciet opgeruimd worden in disconnectedCallback
disconnectedCallback() {
super.disconnectedCallback();
// Walker cleanup (technisch niet nodig, maar goed voor duidelijkheid)
this.walker = null;
}5. Documenteer complexe traversal logica β
/**
* Verzamelt focusbare elementen in de juiste volgorde:
* 1. Content slot (formulier velden, etc.)
* 2. Actions slot (custom buttons)
* 3. Shadow DOM footer (prebuilt buttons)
*
* Headers worden bewust overgeslagen voor autofocus.
*
* @returns Tuple van [eerste focusbare, laatste focusbare] of [null, null]
*/
private _getFirstAndLastFocusableChildren(): [HTMLElement, HTMLElement] | [null, null] {
// Implementatie met TreeWalker...
}Geavanceerde patronen β
1. Herbruikbare traversal utility β
CreΓ«er herbruikbare utilities voor veelvoorkomende traversal patterns:
// _shared/utils/dom-traversal.ts
/**
* Verzamel alle elementen die aan een predicate voldoen
*/
export function findElements(root: Element, predicate: (el: Element) => boolean): HTMLElement[] {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
const result: HTMLElement[] = [];
while (walker.nextNode()) {
const el = walker.currentNode as Element;
if (predicate(el)) {
result.push(el as HTMLElement);
}
}
return result;
}
/**
* Vind het eerste element dat aan predicate voldoet
*/
export function findFirstElement(root: Element, predicate: (el: Element) => boolean): HTMLElement | null {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode as Element;
if (predicate(el)) {
return el as HTMLElement;
}
}
return null;
}
// Gebruik in component
import { findElements, findFirstElement } from '../_shared/utils/dom-traversal';
const focusableElements = findElements(this, (el) => {
return el.tabIndex >= 0 && !(el as HTMLElement).disabled;
});
const firstButton = findFirstElement(this, (el) => {
return el.tagName === 'BUTTON' && !el.hasAttribute('disabled');
});2. Focused traversal met early exit β
Voor betere performance, stop de traversal zodra je hebt wat je nodig hebt:
/**
* Vind eerste focusbaar element (met early exit)
*/
private _findFirstFocusable(): HTMLElement | null {
const walker = document.createTreeWalker(
this.shadowRoot,
NodeFilter.SHOW_ELEMENT
);
while (walker.nextNode()) {
const el = walker.currentNode as Element;
if (el.tagName === 'SLOT') {
const slot = el as HTMLSlotElement;
const assigned = slot.assignedElements({ flatten: true });
for (const assignedEl of assigned) {
const focusable = this._findFirstFocusableInTree(assignedEl);
if (focusable) return focusable; // Early exit!
}
}
else if (this._isFocusable(el)) {
return el as HTMLElement; // Early exit!
}
}
return null;
}3. Traversal met context tracking β
Track extra context tijdens traversal voor complexe use cases:
interface TraversalContext {
depth: number;
parent: Element | null;
index: number;
}
function traverseWithContext(root: Element, callback: (el: Element, context: TraversalContext) => void): void {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let depth = 0;
let parent: Element | null = root;
let index = 0;
while (walker.nextNode()) {
const el = walker.currentNode as Element;
// Track depth changes
if (el.parentElement !== parent) {
if (el.parentElement === walker.currentNode) {
depth++;
} else {
depth--;
}
parent = el.parentElement;
index = 0;
}
callback(el, { depth, parent, index });
index++;
}
}
// Gebruik
traverseWithContext(this, (el, { depth, index }) => {
console.log(`Element ${el.tagName} op depth ${depth}, index ${index}`);
});Performance tips β
1. Batch DOM reads en writes β
// β SLECHT: Interleaved reads en writes (causes layout thrashing)
elements.forEach((el) => {
const height = el.offsetHeight; // READ (forces layout)
el.style.top = height + 'px'; // WRITE
});
// β
GOED: Batch reads, dan writes
const heights = elements.map((el) => el.offsetHeight); // Alle READS
heights.forEach((height, i) => {
// Alle WRITES
elements[i].style.top = height + 'px';
});2. Gebruik requestAnimationFrame voor visual updates β
private _updateFocusStyles(): void {
requestAnimationFrame(() => {
const walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode as HTMLElement;
if (this._isFocusable(el)) {
el.classList.add('focusable-highlight');
}
}
});
}3. Limiteer diepte voor grote trees β
function traverseWithMaxDepth(root: Element, maxDepth: number, callback: (el: Element) => void): void {
let depth = 0;
let currentParent = root;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode as Element;
// Track depth
if (el.parentElement !== currentParent) {
if (el.parentElement?.contains(currentParent)) {
depth++;
} else {
depth--;
}
currentParent = el.parentElement;
}
// Stop bij max depth
if (depth > maxDepth) break;
callback(el);
}
}Samenvatting β
Beslissingsboom β
Moet je DOM elementen vinden?
β
ββ Simpel, bekend element?
β ββ β
Gebruik querySelector/querySelectorAll
β
ββ Direct parent/child?
β ββ β
Gebruik element.parentElement / element.children
β
ββ Runtime property checks nodig?
β ββ β
Gebruik TreeWalker met custom filter
β
ββ Grote tree (>50 elementen) doorlopen?
β ββ β
Gebruik TreeWalker
β
ββ Complexe dynamische logica?
ββ β
Gebruik TreeWalker met predicate functieKey takeaways β
querySelector/querySelectorAll
- Perfect voor simpele, statische selectors
- Vertrouwde CSS syntax
- Beperkt tot wat CSS kan uitdrukken
Manual traversal
- Goed voor directe relaties (parent/children)
- Eenvoudig en direct
- Niet geschikt voor grote trees
TreeWalker
- Beste performance voor complexe traversal
- Runtime property checks mogelijk
- Zero allocations, C++ geoptimaliseerd
- Vereist meer code maar biedt maximale flexibiliteit
De keuze hangt af van je specifieke use case - gebruik de eenvoudigste tool die het werk doet, maar schakel over naar TreeWalker wanneer je complexe logica of optimale performance nodig hebt.