React Profiler — встроенный инструмент для измерения производительности рендеров:
import { Profiler } from 'react'
function onRenderCallback(
id, // "displayName" Profiler
phase, // "mount" или "update"
actualDuration, // время рендеринга в мс
baseDuration, // расчётное время без мемоизации
startTime, // когда React начал рендеринг
commitTime // когда React зафиксировал рендеринг
) {
console.log(`[${id}] ${phase}: ${actualDuration.toFixed(2)}мс`)
}
function App() {
return (
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>
)
}В браузерном расширении React DevTools вкладка Profiler показывает:
Flame chart — визуализация дерева рендеринга:
Ранжирование — компоненты от самого медленного к быстрому.
Как использовать:
1. Открой DevTools → Profiler
2. Нажми "Start profiling"
3. Совершай действия в интерфейсе
4. Нажми "Stop profiling"
5. Исследуй flame chart
Библиотека для обнаружения ненужных ре-рендеров:
// src/wdyr.ts (только для разработки)
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true,
// Или точечно для конкретного компонента:
// logOwnerReasons: true,
})
}
// Включить для конкретного компонента
MyComponent.whyDidYouRender = true// why-did-you-render покажет:
// [MyList] Re-rendered. Причина: изменились пропсы
// Предыдущие: { items: Array(3) }
// Текущие: { items: Array(3) } ← ОДИНАКОВЫЕ! Значение то же, но новая ссылка# Для Create React App
npx source-map-explorer 'build/static/js/*.js'
# Для Vite
npm install rollup-plugin-visualizer -D// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
})
]
}Что искать в анализе бандла:
import { lazy, Suspense } from 'react'
// Ленивая загрузка: компонент загрузится только когда понадобится
const HeavyChart = lazy(() => import('./HeavyChart'))
const AdminPanel = lazy(() => import('./AdminPanel'))
function App() {
const isAdmin = useUser().role === 'admin'
return (
<Suspense fallback={<div>Загрузка...</div>}>
<HeavyChart data={chartData} />
{isAdmin && <AdminPanel />}
</Suspense>
)
}
// Роутинг с ленивой загрузкой страниц
const HomePage = lazy(() => import('./pages/HomePage'))
const BlogPage = lazy(() => import('./pages/BlogPage'))
const ProfilePage = lazy(() => import('./pages/ProfilePage'))
function Router() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Routes>
</Suspense>
)
}React Compiler автоматически добавляет мемоизацию — больше не нужно вручную писать useMemo и useCallback:
npm install babel-plugin-react-compiler -D// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
target: '18', // совместимость с React 18
}]
]
}// До React Compiler (ручная мемоизация)
function ProductList({ products, onBuy }) {
const sorted = useMemo(
() => [...products].sort((a, b) => a.price - b.price),
[products]
)
const handleBuy = useCallback(
(id) => onBuy(id),
[onBuy]
)
return sorted.map(p => <Product key={p.id} product={p} onBuy={handleBuy} />)
}
// После React Compiler — он добавит мемоизацию автоматически!
function ProductList({ products, onBuy }) {
const sorted = [...products].sort((a, b) => a.price - b.price)
return sorted.map(p => <Product key={p.id} product={p} onBuy={(id) => onBuy(id)} />)
}// 1. Виртуализация списков (react-virtual или react-window)
import { useVirtualizer } from '@tanstack/react-virtual'
function BigList({ items }) {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
})
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
)
}
// 2. Дебаунс для часто срабатывающих обработчиков
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedSearch = useMemo(
() => debounce((q) => fetchResults(q), 300),
[]
)
return (
<input
value={query}
onChange={e => {
setQuery(e.target.value)
debouncedSearch(e.target.value)
}}
/>
)
}Профайлер рендеров на ванильном JS: отслеживание количества рендеров, времени выполнения и выявление лишних повторных рендеров
// Симулируем React Profiler: отслеживаем рендеры компонентов,
// измеряем время и находим ненужные повторные вызовы.
// --- Профайлер ---
function createProfiler() {
const stats = new Map() // componentName -> { renderCount, totalTime, renders[] }
return {
// Записать рендер компонента
track(componentName, renderFn, props) {
const start = performance.now()
const result = renderFn(props)
const duration = performance.now() - start
if (!stats.has(componentName)) {
stats.set(componentName, { renderCount: 0, totalTime: 0, renders: [] })
}
const s = stats.get(componentName)
s.renderCount++
s.totalTime += duration
s.renders.push({ duration, props: JSON.stringify(props) })
return result
},
// Получить статистику
getStats() {
const result = []
for (const [name, s] of stats) {
result.push({
componentName: name,
renderCount: s.renderCount,
totalTime: Math.round(s.totalTime * 100) / 100,
avgTime: Math.round((s.totalTime / s.renderCount) * 100) / 100,
})
}
return result.sort((a, b) => b.totalTime - a.totalTime)
},
// Найти подозрительные повторные рендеры с теми же пропсами
findUnnecessaryRenders() {
const suspicious = []
for (const [name, s] of stats) {
if (s.renders.length < 2) continue
for (let i = 1; i < s.renders.length; i++) {
if (s.renders[i].props === s.renders[i - 1].props) {
suspicious.push({
component: name,
renderIndex: i,
message: 'Одинаковые пропсы: ' + s.renders[i].props,
})
}
}
}
return suspicious
},
reset() { stats.clear() },
}
}
// --- Тестовые "компоненты" ---
function Header({ title }) {
// Имитируем тяжёлый компонент
let sum = 0
for (let i = 0; i < 10000; i++) sum += i
return { tag: 'header', content: title, checksum: sum % 100 }
}
function Button({ label, variant }) {
return { tag: 'button', label, variant }
}
function Avatar({ userId }) {
let hash = 0
for (let i = 0; i < 5000; i++) hash ^= i
return { tag: 'img', src: '/avatars/' + userId, hash }
}
// --- Профилирование ---
const profiler = createProfiler()
// Нормальные рендеры
profiler.track('Header', Header, { title: 'Главная' })
profiler.track('Button', Button, { label: 'ОК', variant: 'primary' })
profiler.track('Avatar', Avatar, { userId: 42 })
// Симулируем ненужные ре-рендеры (те же пропсы!)
profiler.track('Button', Button, { label: 'ОК', variant: 'primary' })
profiler.track('Avatar', Avatar, { userId: 42 })
// Легитимный ре-рендер с другими пропсами
profiler.track('Button', Button, { label: 'Отмена', variant: 'secondary' })
profiler.track('Header', Header, { title: 'О нас' })
// --- Анализ ---
console.log('=== Статистика рендеров ===')
profiler.getStats().forEach(s => {
console.log(s.componentName + ':')
console.log(' Рендеров:', s.renderCount)
console.log(' Всего:', s.totalTime + 'мс')
console.log(' В среднем:', s.avgTime + 'мс')
})
console.log('
=== Подозрительные повторные рендеры ===')
const suspicious = profiler.findUnnecessaryRenders()
if (suspicious.length === 0) {
console.log('Ненужных рендеров не обнаружено')
} else {
suspicious.forEach(s => {
console.log('[!] ' + s.component + ' (рендер #' + s.renderIndex + ')')
console.log(' ' + s.message)
console.log(' Решение: обернуть в React.memo()')
})
}React Profiler — встроенный инструмент для измерения производительности рендеров:
import { Profiler } from 'react'
function onRenderCallback(
id, // "displayName" Profiler
phase, // "mount" или "update"
actualDuration, // время рендеринга в мс
baseDuration, // расчётное время без мемоизации
startTime, // когда React начал рендеринг
commitTime // когда React зафиксировал рендеринг
) {
console.log(`[${id}] ${phase}: ${actualDuration.toFixed(2)}мс`)
}
function App() {
return (
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>
)
}В браузерном расширении React DevTools вкладка Profiler показывает:
Flame chart — визуализация дерева рендеринга:
Ранжирование — компоненты от самого медленного к быстрому.
Как использовать:
1. Открой DevTools → Profiler
2. Нажми "Start profiling"
3. Совершай действия в интерфейсе
4. Нажми "Stop profiling"
5. Исследуй flame chart
Библиотека для обнаружения ненужных ре-рендеров:
// src/wdyr.ts (только для разработки)
import React from 'react'
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, {
trackAllPureComponents: true,
// Или точечно для конкретного компонента:
// logOwnerReasons: true,
})
}
// Включить для конкретного компонента
MyComponent.whyDidYouRender = true// why-did-you-render покажет:
// [MyList] Re-rendered. Причина: изменились пропсы
// Предыдущие: { items: Array(3) }
// Текущие: { items: Array(3) } ← ОДИНАКОВЫЕ! Значение то же, но новая ссылка# Для Create React App
npx source-map-explorer 'build/static/js/*.js'
# Для Vite
npm install rollup-plugin-visualizer -D// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
})
]
}Что искать в анализе бандла:
import { lazy, Suspense } from 'react'
// Ленивая загрузка: компонент загрузится только когда понадобится
const HeavyChart = lazy(() => import('./HeavyChart'))
const AdminPanel = lazy(() => import('./AdminPanel'))
function App() {
const isAdmin = useUser().role === 'admin'
return (
<Suspense fallback={<div>Загрузка...</div>}>
<HeavyChart data={chartData} />
{isAdmin && <AdminPanel />}
</Suspense>
)
}
// Роутинг с ленивой загрузкой страниц
const HomePage = lazy(() => import('./pages/HomePage'))
const BlogPage = lazy(() => import('./pages/BlogPage'))
const ProfilePage = lazy(() => import('./pages/ProfilePage'))
function Router() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Routes>
</Suspense>
)
}React Compiler автоматически добавляет мемоизацию — больше не нужно вручную писать useMemo и useCallback:
npm install babel-plugin-react-compiler -D// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
target: '18', // совместимость с React 18
}]
]
}// До React Compiler (ручная мемоизация)
function ProductList({ products, onBuy }) {
const sorted = useMemo(
() => [...products].sort((a, b) => a.price - b.price),
[products]
)
const handleBuy = useCallback(
(id) => onBuy(id),
[onBuy]
)
return sorted.map(p => <Product key={p.id} product={p} onBuy={handleBuy} />)
}
// После React Compiler — он добавит мемоизацию автоматически!
function ProductList({ products, onBuy }) {
const sorted = [...products].sort((a, b) => a.price - b.price)
return sorted.map(p => <Product key={p.id} product={p} onBuy={(id) => onBuy(id)} />)
}// 1. Виртуализация списков (react-virtual или react-window)
import { useVirtualizer } from '@tanstack/react-virtual'
function BigList({ items }) {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
})
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
)
}
// 2. Дебаунс для часто срабатывающих обработчиков
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedSearch = useMemo(
() => debounce((q) => fetchResults(q), 300),
[]
)
return (
<input
value={query}
onChange={e => {
setQuery(e.target.value)
debouncedSearch(e.target.value)
}}
/>
)
}Профайлер рендеров на ванильном JS: отслеживание количества рендеров, времени выполнения и выявление лишних повторных рендеров
// Симулируем React Profiler: отслеживаем рендеры компонентов,
// измеряем время и находим ненужные повторные вызовы.
// --- Профайлер ---
function createProfiler() {
const stats = new Map() // componentName -> { renderCount, totalTime, renders[] }
return {
// Записать рендер компонента
track(componentName, renderFn, props) {
const start = performance.now()
const result = renderFn(props)
const duration = performance.now() - start
if (!stats.has(componentName)) {
stats.set(componentName, { renderCount: 0, totalTime: 0, renders: [] })
}
const s = stats.get(componentName)
s.renderCount++
s.totalTime += duration
s.renders.push({ duration, props: JSON.stringify(props) })
return result
},
// Получить статистику
getStats() {
const result = []
for (const [name, s] of stats) {
result.push({
componentName: name,
renderCount: s.renderCount,
totalTime: Math.round(s.totalTime * 100) / 100,
avgTime: Math.round((s.totalTime / s.renderCount) * 100) / 100,
})
}
return result.sort((a, b) => b.totalTime - a.totalTime)
},
// Найти подозрительные повторные рендеры с теми же пропсами
findUnnecessaryRenders() {
const suspicious = []
for (const [name, s] of stats) {
if (s.renders.length < 2) continue
for (let i = 1; i < s.renders.length; i++) {
if (s.renders[i].props === s.renders[i - 1].props) {
suspicious.push({
component: name,
renderIndex: i,
message: 'Одинаковые пропсы: ' + s.renders[i].props,
})
}
}
}
return suspicious
},
reset() { stats.clear() },
}
}
// --- Тестовые "компоненты" ---
function Header({ title }) {
// Имитируем тяжёлый компонент
let sum = 0
for (let i = 0; i < 10000; i++) sum += i
return { tag: 'header', content: title, checksum: sum % 100 }
}
function Button({ label, variant }) {
return { tag: 'button', label, variant }
}
function Avatar({ userId }) {
let hash = 0
for (let i = 0; i < 5000; i++) hash ^= i
return { tag: 'img', src: '/avatars/' + userId, hash }
}
// --- Профилирование ---
const profiler = createProfiler()
// Нормальные рендеры
profiler.track('Header', Header, { title: 'Главная' })
profiler.track('Button', Button, { label: 'ОК', variant: 'primary' })
profiler.track('Avatar', Avatar, { userId: 42 })
// Симулируем ненужные ре-рендеры (те же пропсы!)
profiler.track('Button', Button, { label: 'ОК', variant: 'primary' })
profiler.track('Avatar', Avatar, { userId: 42 })
// Легитимный ре-рендер с другими пропсами
profiler.track('Button', Button, { label: 'Отмена', variant: 'secondary' })
profiler.track('Header', Header, { title: 'О нас' })
// --- Анализ ---
console.log('=== Статистика рендеров ===')
profiler.getStats().forEach(s => {
console.log(s.componentName + ':')
console.log(' Рендеров:', s.renderCount)
console.log(' Всего:', s.totalTime + 'мс')
console.log(' В среднем:', s.avgTime + 'мс')
})
console.log('
=== Подозрительные повторные рендеры ===')
const suspicious = profiler.findUnnecessaryRenders()
if (suspicious.length === 0) {
console.log('Ненужных рендеров не обнаружено')
} else {
suspicious.forEach(s => {
console.log('[!] ' + s.component + ' (рендер #' + s.renderIndex + ')')
console.log(' ' + s.message)
console.log(' Решение: обернуть в React.memo()')
})
}Создай React компонент OptimizedList, демонстрирующий оптимизацию с memo, useMemo и useCallback. Компонент должен: иметь список элементов items в состоянии, использовать useMemo для фильтрации/сортировки списка, использовать useCallback для обработчика удаления, передавать обработчик в мемоизированный дочерний компонент ListItem (обёрнутый в memo).
В useMemo: filter.toLowerCase() для фильтрации, a.priority - b.priority для сортировки, зависимости [items, filter]. В useCallback: item.id !== id для фильтрации. При добавлении: [...prev, newItem]. onChange: e.target.value. В map: key={item.id}, item={item}, onDelete={handleDelete}.