---
title: "Keyed vs Unkeyed Lists"
slug: keyed-lists-performance
section: guides
order: 26
level: advanced
description: "Why dj-key matters: what the diff does when a list reorders without one."
---

# List Reordering Performance: Keyed vs Unkeyed Lists

## 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):**
```html
{% 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):**
```html
{% 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:

```html
<!-- 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:

```bash
DJUST_VDOM_TRACE=1 python manage.py runserver
```

Or in `settings.py`:
```python
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](../../VDOM_TRACING.md) — Full tracing documentation
- [VDOM Torture Test Report](../../VDOM_TORTURE_TEST_REPORT.md) — Benchmark data
- [VDOM Architecture Comparison](../../VDOM_ARCHITECTURE_COMPARISON.md) — How djust's diff compares to other frameworks
