Why Ember's Glimmer Engine Makes Incremental DOM Updates a Practical Choice
Glimmer compiles Ember templates to byte‑code that updates only the DOM nodes that actually change. This blog walks through a live list example, shows how to verify incremental updates in the console, and outlines the trade‑offs you’ll face when adopting the new rendering pipeline.
19 Sept 2026, 00:12 UTC

The problem: full re‑renders kill responsiveness
When a data‑heavy Ember app updates a large list or a frequently changing dashboard, the classic view layer tears down and rebuilds the entire subtree. That churn shows up as janky scrolling, high memory pressure, and long paint times. Glimmer solves this by compiling templates to a tiny byte‑code format that knows exactly which DOM nodes can change.
How Glimmer compiles to incremental updates
Glimmer treats each template as a series of opcodes – low‑level instructions like append, update, and remove. At build time the template compiler emits a .gjs (or .gts) module that contains a layout function. At runtime the VM walks that layout, creates the initial DOM once, and then re‑executes only the opcodes whose inputs have changed.
- Fine‑grained dependency tracking – each
{{#each}}block registers areferenceto the array items; when an item mutates, only the correspondingupdateopcode runs. - Block syntax for reusable fragments –
{{#block}}/{{/block}}lets you package a DOM subtree as a first‑class value, so the same byte‑code can be reused across components without re‑compilation.
Worked example: a live‑updating list
Assume a component app/components/user-list.gjs that renders a searchable user table.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class UserList extends Component {
@tracked users = [];
@tracked filter = '';
get filtered() {
return this.users.filter(u => u.name.includes(this.filter));
}
@action
async load() {
const resp = await fetch('/api/users');
this.users = await resp.json();
}
}
Template user-list.hbs:
<input type="text" value={{this.filter}} {{on "input" (fn (mut this.filter) "target.value")}} />
<table>
{{#each this.filtered as |user|}}
<tr>
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.email}}</td>
</tr>
{{/each}}
</table>
When load() replaces the entire users array, Glimmer emits a single replace opcode for the <tbody> fragment. When the user types in the filter box, only the rows that disappear or appear receive remove / append opcodes – the unchanged rows stay untouched.
Verification step: open the browser devtools console and run:
import { debugRenderTree } from '@glimmer/debug';
debugRenderTree(document.querySelector('user-list'));
The output shows a tree of Opcode objects; you should see UpdateOpcode entries only for the rows that changed.
Trade‑offs and limitations
- Learning curve – developers must understand the block syntax (
{{#block}}) and the new@glimmer/trackingprimitives. Existing classic components usingEmber.Object.extendand the run‑loop will not automatically benefit. - Add‑on compatibility – older add‑ons that manipulate the DOM directly in
didInsertElementcan break because Glimmer expects the VM to own the DOM lifecycle. - Debugging diff output – the opcode log is more verbose than the old view hierarchy; you may need the
@glimmer/debughelpers to make sense of it.
How to confirm the benefit in your project
- Upgrade to Ember 3.0+ (Glimmer shipped in 3.0).
- Convert a high‑traffic component to a Glimmer component (use
ember generate component --glimmer). - Run a performance profile (Chrome DevTools → Performance) before and after the conversion. Look for reduced Scripting time and fewer Layout events during list updates.
- Optionally, add a micro‑benchmark: render 10 000 rows, mutate 1 % of them, and measure frame time. Glimmer typically stays under 16 ms per frame; classic views often exceed 30 ms.
If the numbers improve and the opcode log shows only targeted updates, you have concrete evidence that the incremental DOM path is paying off.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.