Перейти к содержимому

25. Производительность и Fine-Grained Reactivity

Привет! 👋 Одна из главных причин, по которой разработчики выбирают Solid.js — его феноменальная производительность. Но чтобы получить максимум, нужно понимать как работает реактивность “под капотом”.

Думай о реактивности Solid как о хирурге с лазерным скальпелем: вместо того чтобы перерисовывать весь компонент (как React), он точечно обновляет только нужный DOM-узел. Никаких лишних операций.


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 использует push-pull реактивность с автоматическим отслеживанием:

// Во время выполнения createEffect/createMemo/JSX-выражения
// Solid "прислушивается" к каждому вызову сигнала
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
// При первом выполнении createMemo:
// → вызов a() → регистрирует зависимость от a
// → вызов b() → регистрирует зависимость от b
const 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!)
});
// Зависимости пересчитываются при каждом выполнении!

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()); // ← правильно
// ❌ Слишком частые обновления без batch
function rapidUpdates() {
for (let i = 0; i < 100; i++) {
setCount(n => n + 1); // 100 обновлений DOM!
}
}
// ✅ Группируй обновления через batch
import { batch } from 'solid-js';
function batchedUpdates() {
batch(() => {
for (let i = 0; i < 100; i++) {
setCount(n => n + 1); // 1 обновление DOM
}
});
}

🎯 Playground: Визуализатор производительности

Заголовок раздела «🎯 Playground: Визуализатор производительности»