Архитектура App Router в Next.js требует строгого разделения на Server Components (RSC) и Client Components. Часто разработчики злоупотребляют директивой 'use client', помещая её на верхнем уровне приложения. Это приводит к каскадному заражению (Client Component Contamination): все дочерние элементы автоматически становятся клиентскими, раздувая JavaScript бандл и ухудшая производительность.
Использование AI агентов для анализа дерева компонентов позволяет автоматизировать выявление таких утечек. Внедрение Donut Pattern (паттерн "Пончик") изолирует клиентскую интерактивность (дырка от пончика), оставляя большую часть обертки (само тесто) серверной, что критически снижает вес загружаемого JavaScript.
Представьте дерево компонентов. Если корень помечен как 'use client', весь подграф отправляется в бандл.
[ Плохо: Утечка ] [ Хорошо: Donut Pattern ]
'use client' (Page) (Server Component)
| |
[HeavyLibrary] [Client Interactive] (Дырка от пончика)
| |
[Static Text] [children: Server Component] (Внедрение через пропсы)children.
Запуск @next/bundle-analyzer для выявления тяжелых чанков.
AI анализирует дерево AST, находя компоненты с 'use client', у которых есть тяжелые неинтерактивные дочерние элементы.
Извлечение логики состояния (useState, useEffect) в отдельный файл-обертку.
Передача статических компонентов в клиентскую обертку через проп children.
Повторный прогон Bundle Analyzer, проверка отсутствия ошибок сериализации.
Каким образом паттерн 'Donut' позволяет включить серверные компоненты (RSC) внутрь клиентского дерева без раздувания бандла?
Ниже представлен пример того, как неоптимизированный код рефакторится в Donut Pattern.
'use client'; // Утечка на уровень всего дашборда
import { useState } from 'react';
import { HeavyChart } from 'heavy-chart-lib';
import { StaticSidebar } from './StaticSidebar';
import { Header } from './Header';
export function Dashboard({ data }: { data: any }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Header />
<StaticSidebar /> {/* Статика попала в клиентский бандл */}
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <HeavyChart data={data} />}
</div>
);
}// Server Component по умолчанию
import { StaticSidebar } from '@/components/StaticSidebar';
import { Header } from '@/components/Header';
import { ClientToggleWrapper } from '@/components/ClientToggleWrapper';
export default function DashboardPage({ data }: { data: any }) {
return (
<div>
<Header />
<StaticSidebar />
{/*
ClientToggleWrapper - это "дырка",
но внутрь нее можно передать RSC (дети)
*/}
<ClientToggleWrapper data={data} />
</div>
);
}'use client'; // Интерактивность изолирована
import { useState } from 'react';
import { HeavyChart } from 'heavy-chart-lib';
export function ClientToggleWrapper({ data }: { data: any }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <HeavyChart data={data} />}
</>
);
}Какой инструмент гарантирует, что модуль с прямым доступом к базе данных или секретным API-ключам никогда случайно не будет импортирован в Client Component?
Передавая RSC в клиентский компонент через children, убедитесь, что вы не пытаетесь пробросить функции, классы или Date объекты в пропсы клиентского компонента. RSC и Client Components общаются через строгий барьер сериализации.
Даже если вы не используете серверную функцию в компоненте, просто её импорт (например, import { db } from '@/db') в файле с 'use client' приведет к ошибке сборки, так как Next.js попытается включить серверный код (с ключами БД) в бандл.
Хотя Server Actions позволяют обрабатывать формы без JS, для прогрессивного улучшения (useActionState, useFormStatus) или сложной клиентской валидации обертка формы должна оставаться клиентским компонентом.