List Performance and Key Strategy

Difficulty: Intermediate

When React needs to update a list, it uses a process called reconciliation to determine the minimum number of DOM operations required. React compares the old list of elements with the new list, matching elements by their keys. If two elements have the same key, React assumes they represent the same item and only updates changed props. If a key is new, React mounts a new element. If a key disappears, React unmounts that element. This is why keys are so critical - they drive the entire update algorithm.

The reconciliation algorithm works in linear time O(n) by making two assumptions: elements with different types are completely different trees, and keys tell React which elements are the same across renders. Without keys, React compares elements by position - the first element in the old list is compared with the first element in the new list. This positional comparison fails when items are inserted, removed, or reordered, because the positions no longer correspond to the same items.

Using array indices as keys is the most common performance and correctness pitfall. When an item is added to the beginning of a list, every subsequent item's index shifts by one. React sees that key 0 now has different content, key 1 has different content, and so on - it updates every single item instead of just inserting one. Even worse, if list items have internal state (like form inputs), the state gets attached to the wrong items because the key-to-element mapping is wrong.

Stable keys from your data (like database IDs) solve these problems. When an item is inserted, existing items keep their keys, so React knows they have not changed. When items are reordered, their keys move with them, so React simply repositions the DOM nodes. This is dramatically more efficient and correct. If your data does not have natural IDs, generate them when the data is created (not during rendering), using something like a UUID or an incrementing counter.

For performance-critical lists with hundreds or thousands of items, consider virtualization libraries like react-window or react-virtuoso. These libraries only render the items currently visible in the viewport, plus a small buffer. Instead of creating 10,000 DOM nodes, you might only render 30. This reduces memory usage, improves rendering speed, and makes scrolling smooth. Virtualization combined with stable keys gives you optimal list performance.

Code examples

Reconciliation with Keys

// Simulating React's reconciliation logic
function reconcile(oldList, newList) {
  const operations = [];
  const oldMap = {};
  oldList.forEach(item => { oldMap[item.key] = item; });

  newList.forEach((newItem, index) => {
    const oldItem = oldMap[newItem.key];
    if (!oldItem) {
      operations.push('INSERT: ' + newItem.key + ' ("' + newItem.text + '")');
    } else if (oldItem.text !== newItem.text) {
      operations.push('UPDATE: ' + newItem.key + ' ("' + oldItem.text + '" -> "' + newItem.text + '")');
    }
    delete oldMap[newItem.key];
  });

  Object.keys(oldMap).forEach(key => {
    operations.push('REMOVE: ' + key);
  });

  return operations;
}

const oldList = [
  { key: 'a', text: 'Apple' },
  { key: 'b', text: 'Banana' },
  { key: 'c', text: 'Cherry' }
];

const newList = [
  { key: 'b', text: 'Banana' },
  { key: 'a', text: 'Avocado' },
  { key: 'd', text: 'Date' }
];

const ops = reconcile(oldList, newList);
console.log('Operations needed:');
ops.forEach(op => console.log('  ' + op));

With stable keys, React can identify that 'b' is unchanged, 'a' was updated, 'd' is new, and 'c' was removed. Only 3 operations instead of rebuilding everything.

Index Keys vs Stable Keys

// Demonstrate why index keys cause problems on insert
function simulateWithIndexKeys(items) {
  return items.map((item, index) => ({ key: index, text: item }));
}

function simulateWithIdKeys(items) {
  return items.map(item => ({ key: item.id, text: item.text }));
}

const original = [
  { id: 'a', text: 'First' },
  { id: 'b', text: 'Second' }
];

// Insert at beginning
const afterInsert = [
  { id: 'c', text: 'New First' },
  { id: 'a', text: 'First' },
  { id: 'b', text: 'Second' }
];

console.log('Index keys before:');
simulateWithIndexKeys(original.map(i => i.text))
  .forEach(i => console.log('  key=' + i.key + ': ' + i.text));

console.log('Index keys after insert:');
simulateWithIndexKeys(afterInsert.map(i => i.text))
  .forEach(i => console.log('  key=' + i.key + ': ' + i.text));

console.log('Result: key 0 changed content, key 1 changed content (BAD)');

console.log('---');
console.log('ID keys before:');
simulateWithIdKeys(original)
  .forEach(i => console.log('  key=' + i.key + ': ' + i.text));

console.log('ID keys after insert:');
simulateWithIdKeys(afterInsert)
  .forEach(i => console.log('  key=' + i.key + ': ' + i.text));

console.log('Result: key c is new, keys a and b unchanged (GOOD)');

Index keys shift when items are inserted, making React think existing items changed. ID keys remain stable, so React correctly identifies the new item.

Generating Stable Keys

// Pattern 1: Use a counter for locally generated items
let nextId = 1;
function createItem(text) {
  return { id: 'item-' + (nextId++), text };
}

const items = [];
items.push(createItem('Learn React'));
items.push(createItem('Build project'));
items.push(createItem('Deploy app'));

console.log('Generated items:');
items.forEach(item => console.log('  ' + item.id + ': ' + item.text));

// Pattern 2: Composite keys from data properties
const events = [
  { date: '2026-01-15', type: 'meeting', title: 'Standup' },
  { date: '2026-01-15', type: 'deadline', title: 'Sprint end' },
  { date: '2026-01-16', type: 'meeting', title: 'Retro' }
];

console.log('Composite keys:');
events.forEach(e => {
  const key = e.date + '-' + e.type + '-' + e.title;
  console.log('  key=' + key);
});

Generate IDs at creation time, not render time. Use incrementing counters for local data or composite keys from unique field combinations.

Key points

Concepts covered

reconciliation, stable keys, index as key, key anti-patterns, virtual DOM diffing