25. Производительность и Fine-Grained Reactivity
⚡ Производительность и Fine-Grained Reactivity
Заголовок раздела «⚡ Производительность и Fine-Grained Reactivity»Привет! 👋 Одна из главных причин, по которой разработчики выбирают Solid.js — его феноменальная производительность. Но чтобы получить максимум, нужно понимать как работает реактивность “под капотом”.
Думай о реактивности Solid как о хирурге с лазерным скальпелем: вместо того чтобы перерисовывать весь компонент (как React), он точечно обновляет только нужный DOM-узел. Никаких лишних операций.
🧠 Как работает Fine-Grained Reactivity
Заголовок раздела «🧠 Как работает Fine-Grained Reactivity»React (Virtual DOM подход): Состояние изменилось → Компонент перерендерился (вся функция запустилась) → Создан новый Virtual DOM → Diff старого и нового VDOM → Применение минимальных DOM-изменений
Цена: функция компонента + VDOM diff + reconciliation
Solid.js (Fine-Grained Reactivity): Сигнал изменился → Все подписчики этого сигнала получают уведомление → Только зависимые DOM-выражения обновляются → Прямая запись в DOM (без VDOM!)
Цена: обновление только изменённых узловВ Solid компонент — это функция-фабрика, которая запускается один раз при создании и возвращает DOM. Потом DOM обновляется напрямую через реактивные связи:
function Counter() { const [count, setCount] = createSignal(0);
// ↓ Эта функция запускается ОДИН РАЗ // JSX компилируется в прямые DOM-операции return ( <div> {/* Solid создаёт текстовый узел, подписанный на count() */} <span>{count()}</span> {/* При count() изменении — обновляется ТОЛЬКО этот текстовый узел */} <button onClick={() => setCount(n => n + 1)}>+</button> </div> );}// При изменении count — функция Counter() НЕ вызывается повторно!🔬 Как Solid отслеживает зависимости
Заголовок раздела «🔬 Как Solid отслеживает зависимости»Solid использует push-pull реактивность с автоматическим отслеживанием:
// Во время выполнения createEffect/createMemo/JSX-выражения// Solid "прислушивается" к каждому вызову сигнала
const [a, setA] = createSignal(1);const [b, setB] = createSignal(2);
// При первом выполнении createMemo:// → вызов a() → регистрирует зависимость от a// → вызов b() → регистрирует зависимость от bconst sum = createMemo(() => a() + b());// Теперь sum зависит от a и b
// Динамические зависимости:const [useA, setUseA] = createSignal(true);
const dynamic = createMemo(() => { if (useA()) { return a(); // ← зависимость от useA И a } return b(); // ← при useA=false: зависимость от useA и b (не a!)});// Зависимости пересчитываются при каждом выполнении!🎯 keyed vs non-keyed в компоненте For
Заголовок раздела «🎯 keyed vs non-keyed в компоненте For»import { For, Index } from 'solid-js';
const items = ['А', 'Б', 'В'];
// For — KEYED (каждый элемент получает реактивный accessor)// При изменении порядка — Solid переиспользует DOM-узлы<For each={items()}> {(item, index) => ( // item — строка (не сигнал!), index — Accessor<number> <li>{item} at {index()}</li> )}</For>
// Index — NON-KEYED (позиционные узлы)// Каждая позиция сохраняет DOM-узел, item становится Accessor<Index each={items()}> {(item, index) => ( // item — Accessor<string>!, index — number (не сигнал) <li>{item()} at {index}</li> )}</Index>Когда использовать:
<For>— объекты с ID, часто меняющийся порядок (drag & drop)<Index>— примитивные значения, фиксированная длина
🧊 Lazy Evaluation: createMemo не пересчитывает без подписчиков
Заголовок раздела «🧊 Lazy Evaluation: createMemo не пересчитывает без подписчиков»const [count, setCount] = createSignal(0);
// ЛЕНИВЫЙ: createMemo не вычисляется пока нет подписчиковconst expensive = createMemo(() => { console.log('Вычисляю...'); // НЕ логируется без подписчика! return count() * 1000 * Math.PI;});
// Только при чтении expensive() — вычисление происходитcreateEffect(() => { console.log(expensive()); // Теперь логируется!});
// Кеширование: createMemo не пересчитывает если значение не изменилосьconst isEven = createMemo(() => count() % 2 === 0);// Если count() меняется с 2 на 4 — isEven() всё равно true// Подписчики isEven() НЕ получат уведомление!⚡ Оптимизация: избегай избыточной реактивности
Заголовок раздела «⚡ Оптимизация: избегай избыточной реактивности»// ❌ Проблема: сигнал в цикле читается много разfunction List({ items }: { items: Accessor<string[]> }) { return ( <div> {/* ✅ Solid компилирует это в For — правильно */} <For each={items()}> {(item) => <div>{item}</div>} </For> </div> );}
// ❌ Создание лишних вычисленийfunction BadComponent() { const [count, setCount] = createSignal(0);
// ❌ Каждое выражение в JSX — отдельная подписка // Три подписки на одни и те же данные const a = createMemo(() => count() * 1); const b = createMemo(() => count() * 2); const c = createMemo(() => count() * 3);
return <div>{a()} {b()} {c()}</div>;}
// ✅ Группируй связанные вычисленияfunction GoodComponent() { const [count, setCount] = createSignal(0);
const derived = createMemo(() => ({ a: count() * 1, b: count() * 2, c: count() * 3, }));
return <div>{derived().a} {derived().b} {derived().c}</div>;}
// ✅ Используй untrack() чтобы читать сигнал без подпискиimport { untrack } from 'solid-js';
createEffect(() => { const current = count(); // ← подписываемся const snapshot = untrack(() => otherSignal()); // ← читаем без подписки console.log(current, snapshot);});📊 Профилирование производительности
Заголовок раздела «📊 Профилирование производительности»// Метрика 1: Считаем DOM-обновленияlet domUpdates = 0;const observer = new MutationObserver(mutations => { domUpdates += mutations.length; console.log(`DOM обновлений: ${domUpdates}`);});observer.observe(document.body, { childList: true, subtree: true, characterData: true });
// Метрика 2: createMemo с измерениемfunction measureMemo<T>(name: string, fn: () => T) { return createMemo(() => { const start = performance.now(); const result = fn(); const elapsed = performance.now() - start; if (elapsed > 1) console.warn(`${name}: ${elapsed.toFixed(2)}ms`); return result; });}
// Метрика 3: Подсчёт ре-рендеров компонентаfunction withRenderCount<T>(Component: Component<T>) { let count = 0; return (props: T) => { count++; console.log(`${Component.name} renders: ${count}`); return <Component {...props} />; };}
// Совет: DevTools для Solid// 1. solid-devtools (npm) — инспектор компонентов и сигналов// 2. Chrome DevTools Performance — записывай профиль// 3. Lighthouse — общая производительность⚠️ Типичные ловушки производительности
Заголовок раздела «⚠️ Типичные ловушки производительности»// ❌ Создание сигнала внутри createEffect — цикл!createEffect(() => { const count = someSignal(); // ❌ Создаём новый сигнал при каждом выполнении эффекта const [local, setLocal] = createSignal(count);});
// ✅ Сигналы создаются вне эффектовconst [local, setLocal] = createSignal(0);createEffect(() => { setLocal(someSignal());});
// ❌ Чтение сигнала вне реактивного контекста — нет подпискиconst count = mySignal(); // ← читает значение ОДИН РАЗ// Никакого обновления при изменении!
// ✅ Читай сигналы внутри JSX или реактивных примитивовconst value = createMemo(() => mySignal()); // ← правильно
// ❌ Слишком частые обновления без batchfunction rapidUpdates() { for (let i = 0; i < 100; i++) { setCount(n => n + 1); // 100 обновлений DOM! }}
// ✅ Группируй обновления через batchimport { batch } from 'solid-js';function batchedUpdates() { batch(() => { for (let i = 0; i < 100; i++) { setCount(n => n + 1); // 1 обновление DOM } });}