💡 Довідник · Гайд для контриб'юторів
📅 Липень 2026 ⏱ Довідник 🎯 QA перед мерджем

Чек-лист запуску симуляції

Робочий чек-лист, який має пройти кожен контриб'ютор перед тим, як нова симуляція з'явиться на цьому сайті: математика бюджету кадру, обробка DPR і resize, відновлення після втрати WebGL-контексту, звільнення пам'яті, доступність, крос-девайс QA і метадані, від яких залежить, чи знайдуть сторінку взагалі. Кожен непозначений пункт нижче — це блокер для мерджу.

1. Бюджет продуктивності

Перш ніж писати хоч один шейдер, визначте бюджет кадру. При 60 Гц у вас 16.6 мс на кадр; на мобільній панелі з 90 Гц це вже 11.1 мс. Рендерер, крок симуляції та власний композитинг браузера — усі ділять це вікно часу.

Бюджет кадру: T_frame = 1000 / fps_target [мс]
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
Емпіричне правило: якщо симуляція не тримає 30 fps на середньому Android-телефоні (класу Pixel 6a) з дефолтними налаштуваннями, або знизьте дефолтну якість, або сховайте важкий шлях за перемикачем "висока якість", вимкненим за замовчуванням.

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(); // хендл для очищення
}

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);

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?.();
Типовий витік: забути звільнити render-to-texture таргети (WebGLRenderTarget), які використовуються для post-processing проходів — вони утримують повнорозмірні GPU-текстури, і їх легко пропустити, бо вони ніколи не з'являються у видимому графі сцени.

5. Ввід та взаємодія

6. Доступність

Коефіцієнт контрасту: CR = (L1 + 0.05) / (L2 + 0.05)
де 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, що накладається, нечитабельний текст оверлею

8. SEO та метадані

Кожна нова сторінка виходить з коректними, унікальними метаданими — скопійований шаблон з неправильним title/description гірший за його відсутність.

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), 'Виправити перед мерджем');
Рецензентам: сприймайте цей чек-лист як мінімальну планку, а не стелю. Симуляція, яка проходить кожен пункт тут, усе одно може бути відхилена за заплутаність, непривабливий вигляд чи наукову неточність — цей довідник охоплює лише технічний бар'єр запуску.