djust docs
Browse documentation

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

OperationUnkeyed (100 items)Keyed (100 items)
Remove first99 SetText + 1 RemoveChild1 RemoveChild
Full reverse100 SetText~100 MoveChild (no text changes)
Append one1 InsertChild1 InsertChild
Change one in middle1 SetText1 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 keydata-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