List Reordering Performance: Keyed vs Unkeyed Lists
Why dj-key matters: what the diff does when a list reorders without one.
On this page
The Problem
Give each list item a stable key with dj-key="…" (or the equivalent data-key="…"; the examples below use data-key).
When you render a list in a djust template without a key attribute, the VDOM diff algorithm matches children by position. If items reorder (sort, filter, remove from the middle), every shifted item generates a patch — even though the items themselves haven't changed.
Example: Removing the First Item
Template (unkeyed):
{% for task in tasks %}
<li>{{ task.name }}</li>
{% endfor %}
With 100 items, removing the first one produces 99 SetText patches + 1 RemoveChild — the diff sees every position has new text and rewrites them all.
Template (keyed):
{% for task in tasks %}
<li data-key="{{ task.id }}">{{ task.name }}</li>
{% endfor %}
The same operation now produces 1 RemoveChild — the diff matches items by key and knows only one was removed.
Performance Comparison
| Operation | Unkeyed (100 items) | Keyed (100 items) |
|---|---|---|
| Remove first | 99 SetText + 1 RemoveChild | 1 RemoveChild |
| Full reverse | 100 SetText | ~100 MoveChild (no text changes) |
| Append one | 1 InsertChild | 1 InsertChild |
| Change one in middle | 1 SetText | 1 SetText |
Key takeaway: Unkeyed and keyed lists perform identically for appends and single-item edits. The difference only matters when items reorder, get removed from the middle, or get inserted at the beginning.
When to Use data-key
Add data-key when:
- Items can be sorted, filtered, or reordered by the user
- Items can be removed from anywhere in the list (not just the end)
- Items can be inserted at positions other than the end
- The list is large (>20 items) and updates frequently
Skip data-key when:
- The list only ever appends or replaces entirely
- Items never reorder (e.g., a static nav menu)
- The list is small (<10 items) — the overhead difference is negligible
How to Add Keys
Use any stable, unique identifier as the key value:
<!-- Database primary key (most common) -->
{% for product in products %}
<div data-key="{{ product.id }}">{{ product.name }}</div>
{% endfor %}
<!-- UUID or slug -->
{% for page in pages %}
<li data-key="{{ page.slug }}">{{ page.title }}</li>
{% endfor %}
<!-- Composite key for join tables -->
{% for membership in memberships %}
<tr data-key="{{ membership.user_id }}-{{ membership.group_id }}">
...
</tr>
{% endfor %}
Do NOT use the loop index as a key — data-key="{{ forloop.counter }}" is equivalent to unkeyed diffing and provides no benefit.
Only dj-key or data-key enable keyed diffing. An id attribute alone does not: a list keyed only by id is diffed by position.
Debugging
Enable VDOM tracing to see which diff strategy is used and how many patches are generated:
DJUST_VDOM_TRACE=1 python manage.py runserver
Or in settings.py:
LIVEVIEW_CONFIG = {'debug_vdom': True}
The trace prints [VDOM TRACE] lines for each diff step and the patches it emits. There is no dedicated warning for unkeyed lists; a reorder that produces a long run of SetText patches across a list is the sign that it needs keys.
Further Reading
- VDOM Tracing Guide — Full tracing documentation
- VDOM Torture Test Report — Benchmark data
- VDOM Architecture Comparison — How djust's diff compares to other frameworks