Показники платформи, 4 квартал 2027
Контент хвилі 10
Цей девлог завершує хвилю 10, у межах якої вийшло ще два ґрунтовні дописи поряд із цим:
| Допис | Тема | Дата |
|---|---|---|
| 🔥 Spotlight #20 | Термодинаміка та теплопередача | Лис 2027 |
| 🧮 Learning #19 | Алгоритми та обчислювальна складність | Лис 2027 |
| ⚙️ Девлог #29 | 60 кадрів/с на кожному пристрої (цей допис) | Гру 2027 |
Проблема: реальність бюджету кадру
Браузерна симуляція працює всередині requestAnimationFrame,
який спрацьовує з частотою 60 Гц на більшості дисплеїв — даючи
рівно 16,67 мс на кадр. Кожен кадр має встигнути обробити фізику,
виявлення зіткнень і рендеринг. На сучасному настільному
комп'ютері з дискретною відеокартою цей бюджет цілком комфортний.
На середньому телефоні зразка 2019 року він уже тісний. А на
бюджетному пристрої з повільним JavaScript-двигуном це обмеження,
яке безжально карає будь-яке алгоритмічне марнотратство.
До спринту оптимізації 4 кварталу 2027 року 47 наших симуляцій опускалися нижче 55 кадрів/с на нашому еталонному бюджетному пристрої (Snapdragon 695, 4 ГБ ОЗП, Chrome 119). Після спринту всі 345 симуляцій утримують щонайменше 59 кадрів/с за стандартної кількості частинок, з автоматичним зниженням якості для повільніших пристроїв.
Техніка 1: фіксований крок часу з інтерполяцією
Пастка змінного dt
Більшість навчальних матеріалів із симуляцій інтегрують фізику за
фактичною дельтою кадру:
position += velocity * dt. Це здається логічним —
якщо останній кадр тривав 33 мс замість 16 мс, фізика просуває рух
на ці 33 мс, зберігаючи симуляцію «реальним часом». Але змінний dt
руйнує детермінованість, спричиняє дрейф енергії в симплектичних
інтеграторах і змушує виявлення зіткнень пропускати швидкі об'єкти
(ефект тунелювання).
Патерн ігрового циклу з фіксованим кроком часу
const FIXED_DT = 1 / 60; // крок фізики 16,67 мс
let accumulator = 0;
let prevTime = performance.now();
function loop(now) {
const frameTime = Math.min((now - prevTime) / 1000, 0.25);
prevTime = now;
accumulator += frameTime;
while (accumulator >= FIXED_DT) {
physicsStep(FIXED_DT); // детерміновано, обмежено
accumulator -= FIXED_DT;
}
const alpha = accumulator / FIXED_DT; // коефіцієнт інтерполяції
render(alpha); // інтерполяція між попереднім і поточним станом
requestAnimationFrame(loop);
}
Переваги фіксованого dt:
- Детермінований повтор (той самий вхід → той самий вихід)
- Стабільне збереження енергії (симплектичний Ейлер, Верле)
- Безпечне CCD: перевірка тунелювання на кожному інтервалі FIXED_DT
- Обмежений бюджет фізики навіть на повільних кадрах (максимум 0,25 с)
Вартість:
- Стан потрібно зберігати двічі (попередній + поточний) для інтерполяції
- Додаткова пам'ять: 2× масиви частинок ≈ незначно при типовому n
Обмеження frameTime у 0,25 с — критично важливе: якщо
вкладка згорнута у фон або пристрій перевантажений, ми не
дозволяємо акумулятору вибухнути у «спіраль смерті», яка ставить у
чергу сотні кроків фізики й заморожує сторінку.
Техніка 2: просторове хешування для зіткнень частинок
Від O(n²) до O(n)
Наївне виявлення зіткнень перевіряє кожну частинку проти кожної іншої: O(n²). При 500 частинках це 125 000 перевірок на крок; при 5 000 частинках — 12,5 мільйона. Стандартне рішення — просторове хешування: розділити простір на сітку комірок, призначити кожній частинці свою комірку, а потім перевіряти лише частинки в тій самій або сусідніх комірках.
Просторове хешування — побудова та запит
Розмір комірки: обирайте ≈ 2r (діаметр частинки) для щільного пакування Хеш-функція: cellX = Math.floor(px / cellSize) cellY = Math.floor(py / cellSize) hash = ((cellX * 73856093) ^ (cellY * 19349663)) % tableSize Фаза побудови (кожен кадр): O(n): перебрати всі частинки, вставити в хеш-таблицю Використовуйте плаский Uint32Array (уникайте навантаження на GC від об'єктних мап) Фаза запиту (для кожної частинки): Перевірити 9 сусідніх комірок (околиця 3×3) Середня кількість сусідів: 4-9 частинок (щільно) проти 0-1 (розріджено) Найгірший випадок: O(n), лише якщо всі частинки в одній комірці Очікувана складність: O(n) побудова + O(n·k) запит, k = середня кількість сусідів k ≈ π·(2r)²·ρ = константа для фіксованої густини → загалом O(n) Розташування в пам'яті (дружнє до кешу): pos[0..n]: x₀,y₀,x₁,y₁,… (перемежовано, Float32Array) vel[0..n]: vx₀,vy₀,vx₁,vy₁,… (перемежовано, Float32Array) Уникає "гонитви за вказівниками", тримає L1/L2 кеш гарячим під час ітерації
У наших симуляціях частинок (Бульбашки, Дим, Броунівський рух, Молекулярна динаміка) ця зміна скоротила час виявлення зіткнень з ~8 мс до ~0,4 мс для 2000 частинок — прискорення у 20 разів, яке звільнило бюджет кадру для більшої кількості частинок і якіснішого рендерингу.
Техніка 3: винесення фізики у Web Worker
Звільнення основного потоку
Основний потік браузера обробляє JavaScript, розмітку,
відмальовування, події вводу та скидання буфера Canvas/WebGL.
Конкуренція з усім цим за бюджет фізики спричиняє «смикання» —
помітні ривки навіть тоді, коли середній час кадру вкладається в
бюджет, бо раптова пауза збирача сміття або подія DOM може вкрасти
2–5 мс. Рішення — виконувати фізику в окремому Web Worker,
обмінюючись станом через SharedArrayBuffer.
Архітектура Web Worker + SharedArrayBuffer
Основний потік Воркер фізики
──────────────────────────────────────────────────────
SharedArrayBuffer (SAB):
positionsBuf: Float32Array(n*2)
velocitiesBuf: Float32Array(n*2)
controlBuf: Int32Array(4) [крок, готово, пауза, n]
// Основний потік: запуск воркера
const worker = new Worker('physics.js');
worker.postMessage({ sab: positionsBuf.buffer, ... });
// requestAnimationFrame: рендер останнього стану
function render(alpha) {
Atomics.wait(controlBuf, 1, 0); // чекати прапорець "готово"
drawParticles(positionsBuf);
Atomics.store(controlBuf, 0, 1); // сигнал: наступний крок
}
// physics.js Worker: цикл фізики
self.onmessage = ({ data }) => {
const pos = new Float32Array(data.sab);
while (true) {
Atomics.wait(data.ctrl, 0, 0); // чекати сигнал кроку
physicsStep(pos, vel, FIXED_DT);
Atomics.store(data.ctrl, 1, 1); // сигнал: готово
}
};
Переваги:
- Фізика ніколи не блокує ввід чи відмальовування
- Використовує друге ядро процесора на всіх сучасних телефонах
- Паузи GC в основному потоці не гальмують фізику
Застереження:
- SharedArrayBuffer вимагає заголовків COOP/COEP
- Atomics.wait() блокує потік, що викликає — використовуйте обережно
- Без серіалізації: без витрат на структуроване клонування
Вимога безпеки:
SharedArrayBuffer потребує HTTP-заголовків
Cross-Origin-Opener-Policy: same-origin та
Cross-Origin-Embedder-Policy: require-corp. Вони
налаштовані в конфігурації Cloudflare Pages для всіх маршрутів
симуляцій.
Техніка 4: адаптивна якість залежно від батареї
Тримаємо телефони прохолодними
Фізика частинок на 60 кадрів/с швидко виснажує батарею телефону і провокує термічне дроселювання — тактова частота CPU/GPU пристрою падає, щоб розсіяти тепло, спричиняючи різкий обвал FPS, який гірший за стабільно нижчу продуктивність. Наша система, що враховує стан батареї, визначає рівень заряду та статус підключення до мережі через Battery Status API і автоматично регулює кількість частинок та якість симуляції.
Адаптивна якість — інтеграція з Battery API
// Рівні якості
const QUALITY = {
high: { particles: 1.0, substeps: 4, shadows: true },
medium: { particles: 0.6, substeps: 2, shadows: false },
low: { particles: 0.3, substeps: 1, shadows: false },
};
async function selectQuality() {
if (!navigator.getBattery) return 'high';
const bat = await navigator.getBattery();
if (bat.charging) return 'high';
if (bat.level > 0.5) return 'high';
if (bat.level > 0.2) return 'medium';
return 'low';
}
// Резервний варіант за часом кадру (немає Battery API на десктопі)
let slowFrames = 0;
function checkPerformance(dt) {
if (dt > 20) slowFrames++; // 20мс = 50 кадрів/с
else slowFrames = Math.max(0, slowFrames - 1);
if (slowFrames > 60) downscale(); // 1 секунда повільних кадрів
}
// Результат на еталонному пристрої (Snapdragon 695):
// За замовчуванням (high): ~58 кадрів/с, розряд батареї 12%/год
// Адаптивно (medium): ~60 кадрів/с, розряд батареї 7%/год
// Адаптивно (low): ~60 кадрів/с, розряд батареї 4%/год
Техніка 5: оптимізації на стороні рендерингу
Фізика — лише половина бюджету кадру. Виклики відмальовування WebGL та накладні витрати Three.js становлять другу половину у складних сценах. Найбільший вплив мали чотири зміни:
-
Інстансований рендеринг мешів: заміна N окремих
об'єктів
Meshодним викликомInstancedMesh(geometry, material, N), що зменшує кількість викликів відмальовування з N до 1. Критично важливо для симуляцій із великою кількістю частинок (Бульбашки, N-тіл, Зграї). -
Відсіювання за пірамідою видимості для частинок:
пропуск оновлення матриць
InstancedMeshдля частинок поза пірамідою видимості камери. У широких далекодіапазонних симуляціях (Зграї птахів) це усуває до 30% завантажень матриць. -
Подвійна буферизація canvas для 2D-симуляцій:
відмальовування на позаекранному
OffscreenCanvasу воркері, а потімtransferToImageBitmap()в основний потік — без серіалізації, без витрат на відмальовування в основному потоці. -
Відкладене звільнення ресурсів Three.js: виклик
geometry.dispose()таmaterial.dispose()уrequestIdleCallback, а не синхронно при виході зі сцени, що уникає сплеску GC, який спричиняє білий спалах на один кадр.
Результати бенчмарків
До / після — еталонний пристрій (Snapdragon 695)
Симуляція До (FPS) Після (FPS) Застосована техніка ────────────────────────────────────────────────────────────────────── Зграї птахів (500 агентів) 38 60 Просторове хеш + Worker N-тіл (200 тіл) 44 60 Барнс-Гат + Worker Броунівський рух (2000) 29 60 Просторове хеш + SAB Бульбашки (1000) 41 60 Інстансований меш + хеш Леннард-Джонс МД (800) 35 60 Фіксований dt + Worker SPH-рідина (600 точок) 22 58 Хеш + Worker + SAB Клітинний автомат (256²) 60 60 Вже оптимально Сортування (1000 стовпців) 60 60 Чистий JS, без змін Загалом: 47 симуляцій нижче 55 кадрів/с → 0 симуляцій нижче 55 кадрів/с Середній приріст FPS на бюджетному пристрої: +19 кадрів/с
Основні матеріали хвилі 10
Spotlight #20 — Термодинаміка
Spotlight #20 охоплює наші шість симуляцій термодинаміки з акцентом на те, чому ентропія і потік тепла — найглибші закони фізики. Гід проводить через закон охолодження Ньютона, межу ефективності Карно, розподіли швидкостей Максвелла-Больцмана, квантове виправлення Планка для ультрафіолетової катастрофи, формування конвективних структур Бенара та фазові діаграми бінарних сплавів. Кожна симуляція пояснена з перших принципів.
Learning #19 — Алгоритми та складність
Learning #19 об'єднує всі симуляції алгоритмів під кутом обчислювальної складності. Починаючи з нотації Big-O, гід охоплює сортування (наочна різниця O(n²) проти O(n log n)), пошук шляху A* проти Дейкстри (порівняння відвіданих вузлів), пошук із поверненням для задачі N ферзів з поширенням обмежень, NP-складну задачу комівояжера з локальним пошуком 2-opt та генетичні алгоритми, що використовують TSP як ландшафт пристосованості.
Що далі — анонс хвилі 11
1 квартал 2028 року буде зосереджений на двох недостатньо висвітлених розділах каталогу симуляцій: колекції з хімії та колекції із соціальних наук/економіки, які значно розрослися після поштовху прикладних категорій із девлогу #26. Дописи хвилі 11:
- Spotlight #21: хімія та хімічна кінетика — реакційно-дифузійні процеси, горіння, кислотно-лужні рівноваги та спіральні хвилі Білоусова-Жаботинського.
- Learning #20: агентне моделювання — від Зграй птахів до епідеміологічних SIR-моделей, оптимізації мурашиною колонією та емерджентної динаміки дорожнього руху.
- Девлог #30: підсумки року — завершення 2027-го, внесок спільноти та дорожня карта на 2028 рік.