Чек-лист запуску симуляції
Робочий чек-лист, який має пройти кожен контриб'ютор перед тим, як нова симуляція з'явиться на цьому сайті: математика бюджету кадру, обробка DPR і resize, відновлення після втрати WebGL-контексту, звільнення пам'яті, доступність, крос-девайс QA і метадані, від яких залежить, чи знайдуть сторінку взагалі. Кожен непозначений пункт нижче — це блокер для мерджу.
1. Бюджет продуктивності
Перш ніж писати хоч один шейдер, визначте бюджет кадру. При 60 Гц у вас 16.6 мс на кадр; на мобільній панелі з 90 Гц це вже 11.1 мс. Рендерер, крок симуляції та власний композитинг браузера — усі ділять це вікно часу.
60 fps → 16.66 мс 90 fps → 11.11 мс 120 fps → 8.33 мс
Розподіл бюджету (емпіричне правило для однієї сцени):
Крок симуляції (JS) ≤ 30% від T_frame
Малювання + шейдинг на GPU ≤ 50% від T_frame
Запас на GC / композитинг / ввід ≥ 20% від T_frame
Оцінка вартості draw call'ів:
T_gpu ≈ n_draws · c_overhead + Σ(triangles_i / throughput)
де c_overhead ≈ 0.01–0.05 мс на один draw call на інтегрованих GPU
- Профілюйте у панелі Performance Chrome DevTools на пристрої середнього рівня з увімкненим throttling (4× CPU slowdown), а не лише на своїй dev-машині.
-
Тримайте кількість draw call'ів менше ~150 для однієї сцени;
об'єднуйте геометрію або використовуйте
InstancedMeshдля повторюваних об'єктів замість одного mesh на кожен екземпляр. - Орієнтуйтесь на ≤ 500 тис. видимих трикутників на десктопі, ≤ 150 тис. на мобільних — якщо тільки сенс симуляції не полягає саме в кількості трикутників (тоді вкажіть це в описі).
-
Обмежуйте
renderer.setPixelRatio()значенням 2, навіть якщо пристрій повідомляє вищийdevicePixelRatio— приріст якості мінімальний, а навантаження на GPU реальне. - Якщо крок фізики виконується незалежно від рендеру (fixed timestep), перевірте, що він коректно деградує при провалах частоти кадрів, а не входить у спіраль (обмежте акумулятор — не дозволяйте йому нескінченно "наздоганяти").
2. Обробка resize та pixel-ratio
Кожна симуляція має коректно реагувати на зміну розміру вікна, зміну розміру контейнера (якщо вбудована не в повноекранну панель) та зміну орієнтації на мобільних — без витоку слухачів подій і без розтягування render-таргету.
function setupResize(renderer, camera, container) {
const resize = () => {
const width = container.clientWidth;
const height = container.clientHeight;
const dpr = Math.min(window.devicePixelRatio, 2);
renderer.setPixelRatio(dpr);
renderer.setSize(width, height, false); // false = не чіпати CSS-розмір
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
const ro = new ResizeObserver(resize);
ro.observe(container);
resize(); // разовий виклик при ініціалізації
return () => ro.disconnect(); // хендл для очищення
}
-
Використовуйте
ResizeObserverна елементі контейнера, а не глобальний слухачwindow.resize— він спрацьовує й на зміну layout сайдбару/панелі, яка не змінює розмір вікна. -
Завжди передавайте
falseтретім аргументом уsetSize(), щоб three.js не нав'язував inline CSS width/height, що конфліктує з вашими стилями. - Дебаунсьте або обмежуйте через rAF обробник resize, якщо він запускає дорогі операції (пересоздання render-таргету, перебудову post-processing ланцюжка).
- Повертайте й викликайте функцію очищення/disconnect при демонтажі сторінки симуляції (навігація в SPA або повторна ініціалізація під час розробки).
- Перевіряйте зміну орієнтації на реальному телефоні: перехід портрет ↔ альбом не має залишати застарілий aspect ratio, який помітно "стрибає" протягом одного кадру.
3. Відновлення після втрати WebGL-контексту
Мобільні GPU та фонові вкладки забирають WebGL-контекст під навантаженням на пам'ять. Кожна симуляція має пережити це без білого екрана чи спаму в консолі — користувач просто побачить, що сцена з'явилась знову.
canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault(); // обов'язково, інакше контекст не буде відновлюваним
cancelAnimationFrame(rafHandle);
console.warn('[sim] WebGL context lost — рендер-цикл призупинено');
}, false);
canvas.addEventListener('webglcontextrestored', () => {
// Текстури, геометрію та програми треба залити заново —
// three.js робить це автоматично для об'єктів у графі сцени,
// але будь-які вручну кешовані GL-ресурси треба ре-ініціалізувати тут.
initRenderTargets();
renderLoop();
}, false);
-
Викликайте
event.preventDefault()в обробникуwebglcontextlost— без цього браузер вважатиме контекст назавжди втраченим. -
Негайно зупиняйте рендер-цикл (
cancelAnimationFrame) при втраті контексту, щоб уникнути потоку GL-помилок у консолі. -
Пересоздавайте будь-які render-таргети, FBO чи GPU-буфери, якими
керуєте самостійно поза відстежуваними ресурсами three.js, у
webglcontextrestored. -
Примусово протестуйте це розширенням
WEBGL_lose_contextу консолі DevTools:gl.getExtension('WEBGL_lose_context').loseContext(). - Якщо відновлення справді не вдається (деякі дуже старі версії iOS Safari), показуйте дружнє повідомлення "перезавантажте сторінку" замість замороженого кадру.
4. Пам'ять і звільнення ресурсів
Геометрія, матеріали й текстури утримують GPU-буфери, які не прибираються збирачем сміття лише через втрату JS-посилання — їх треба явно звільняти (dispose), інакше повторний перехід на ту саму симуляцію витікатиме VRAM, поки вкладка не впаде.
function disposeScene(root) {
root.traverse((obj) => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
for (const mat of mats) {
for (const key in mat) {
const value = mat[key];
if (value && value.isTexture) value.dispose();
}
mat.dispose();
}
}
});
}
// При демонтажі / зміні маршруту:
cancelAnimationFrame(rafHandle);
disposeScene(scene);
renderer.dispose();
renderer.forceContextLoss();
controls?.dispose?.();
- Звільняйте геометрію, матеріал і кожну текстуру цього матеріалу (map, normalMap, roughnessMap, envMap тощо) — dispose самого матеріалу не звільняє його текстури.
-
Викликайте
renderer.dispose()іrenderer.forceContextLoss()при демонтажі симуляції, особливо в контексті навігації single-page додатку. -
Звільняйте
OrbitControls/кастомні контроли, які підвішують власні слухачі подій на рівні document. -
Перевіряйте
renderer.info.memory(geometries,textures) до і після циклу монтування/демонтажу в DevTools — лічильники мають повертатись до нуля. - Не створюйте нові геометрії/матеріали всередині рендер-циклу — виділяйте один раз, а щокадру лише мутуйте атрибути/uniform'и.
WebGLRenderTarget), які використовуються для
post-processing проходів — вони утримують повнорозмірні
GPU-текстури, і їх легко пропустити, бо вони ніколи не з'являються
у видимому графі сцени.
5. Ввід та взаємодія
-
Використовуйте Pointer Events
(
pointerdown/pointermove/pointerup) замість окремих обробників миші й дотику — один шлях коду для обох. -
Встановлюйте
touch-action: noneна canvas, якщо симуляція перехоплює жести перетягування, щоб браузер не намагався одночасно скролити чи масштабувати сторінку під вашою взаємодією. - Де це можливо, підтримуйте клавіатурні альтернативи для взаємодій, доступних лише через drag (стрілки для зміни значення, Tab для переходу до контролів).
-
Дебаунсьте raycasting на
pointermoveмаксимум до одного разу за кадр анімації — ніколи не запускайте raycast синхронно на кожну DOM-подію. - Обмежуйте орбітальні/зум-контроли розумними межами (мін/макс відстань, полярний кут), щоб користувач не міг завести камеру всередину геометрії чи в нескінченність.
6. Доступність
-
Поважайте
prefers-reduced-motion: за замовчуванням призупиняйте безперервне авто-обертання камери чи idle-анімацію, коли ця media query спрацьовує, з явним контролом для вмикання. -
Давайте canvas доступне ім'я (
aria-labelабо сусідній<h1>), що описує, що показує симуляція, оскільки вміст canvas невидимий для скрінрідерів. -
Кожен елемент керування поза canvas (повзунки, перемикачі,
пресети) має бути досяжним і керованим з клавіатури, з видимими
станами фокусу —
outline: noneбез заміни є блокером. - Дотримуйтесь контрасту WCAG AA (4.5:1 для основного тексту, 3:1 для великого тексту/UI) для всього тексту оверлей-UI на фоні симуляції.
- Надайте статичний fallback (опис або постер-зображення) для браузерів/пристроїв, де WebGL недоступний — ніколи не порожню сторінку.
де L1 = відносна яскравість світлішого кольору, L2 — темнішого, і L1 ≥ L2
WCAG AA: CR ≥ 4.5 (звичайний текст), CR ≥ 3.0 (великий текст ≥ 18pt або 14pt жирний)
7. Крос-браузерне та крос-девайс QA
Перед мерджем вручну перевірте симуляцію за матрицею нижче. Автоматичні перевірки Lighthouse/CI ловлять регресії, але не ловлять зламані drag-взаємодії чи баги втрати контексту.
| Ціль | Чому важливо | На що звертати увагу |
|---|---|---|
| Chrome / Edge (десктоп) | Найбільша частка десктопів, найкраще профілювання в DevTools | Базова лінія — має бути бездоганно |
| Safari (десктоп, macOS) | Інший компілятор WebGL/шейдерів, суворіші обмеження живлення | Помилки компіляції шейдерів, зсуви колірного простору |
| Safari (iOS) | Лише WebKit-рушій на iPhone/iPad, жорстка межа пам'яті | Втрата контексту при згортанні, конфлікт touch-scroll |
| Chrome (Android, середній клас) | Репрезентує медіанний реальний пристрій, а не ваш dev-ноутбук | Частота кадрів, теплове дроселювання після ~2 хв |
| Firefox (десктоп) | Інший стек WebGL-драйверів | Прогалини в підтримці розширень, точність float-текстур |
| Вузький viewport (<380px) | Маленькі телефони, розділений екран | UI, що накладається, нечитабельний текст оверлею |
- Протестуйте зі згорнутою й відновленою вкладкою браузера (втрата контексту, пауза/відновлення rAF) хоча б в одному мобільному браузері.
- Протестуйте з увімкненим налаштуванням ОС "зменшити рух", а не лише з ізольованою media query.
- Переконайтесь, що немає неперехоплених помилок чи попереджень у консолі при завантаженні й взаємодії.
- Перевіряйте мобільний бал продуктивності Lighthouse — позначайте все, що нижче 70, для подальшої роботи, а не мовчки випускайте в реліз.
8. SEO та метадані
Кожна нова сторінка виходить з коректними, унікальними метаданими — скопійований шаблон з неправильним title/description гірший за його відсутність.
-
Унікальні
<title>таmeta name="description", обидва в межах ~60/155 символів відповідно, що описують саме цю симуляцію чи довідник. -
rel="canonical", що вказує на власну EN-URL сторінки, плюс альтернативиhreflangдляen,ukтаx-default. -
Open Graph (
og:title,og:description,og:type,og:image) і відповідна Twitter-картка, щоб посилання коректно рендерились при репостах. -
Структуровані дані (JSON-LD
TechArticle,ArticleчиLearningResource) з реальноюdatePublished— не залишайте дату-заглушку. -
Блок JSON-LD
BreadcrumbList, що відповідає видимій навігації хлібних крихт. - Додайте нову сторінку до відповідної сторінки-індексу/лістингу та в sitemap після завершення контент-рев'ю — цей довідник навмисно не чіпає ці спільні файли.
9. Фінальний чек-лист перед мерджем
Скорочена версія для вставки в опис pull request. Якщо якийсь пункт не застосовний — прямо про це напишіть, а не пропускайте мовчки.
// Чек-лист запуску симуляції — перед мерджем
const checklist = [
'Тримає цільовий fps на мобільному пристрої середнього класу',
'ResizeObserver + зміна орієнтації опрацьовані, немає конфлікту з CSS-розміром',
'devicePixelRatio обмежено значенням 2',
'webglcontextlost / webglcontextrestored опрацьовано й протестовано',
'dispose() викликано для геометрії, матеріалів, текстур, render-таргетів',
'renderer.dispose() + forceContextLoss() при демонтажі',
'Використано Pointer Events; touch-action встановлено коректно',
'prefers-reduced-motion враховано',
'Canvas має доступне ім’я; оверлей-UI керується з клавіатури',
'Контраст WCAG AA для тексту оверлею',
'Немає помилок у консолі при завантаженні чи взаємодії',
'Title, description, canonical, hreflang, OG, Twitter, JSON-LD — усе задано',
'Вручну протестовано на Chrome, Safari desktop, Safari iOS, Android Chrome',
];
console.assert(checklist.every(Boolean), 'Виправити перед мерджем');