Мобільна оптимізація WebGL: кроки та підводні камені
Симуляція, що видає 60fps на десктопній RTX-карті, може впасти до одноцифрових значень — або сама себе термально притормозити до слайдшоу за дві хвилини — на телефоні середнього класу. Мобільні GPU — не менші десктопні GPU; це справді інша архітектура з іншими вузькими місцями. Цей туторіал охоплює конкретні кроки, які дійсно дають ефект, і підводні камені, що проявляються лише коли реальні користувачі відкривають вашу симуляцію на реальних телефонах.
1. Інший режим, а не менший десктопний GPU
Майже кожен мобільний GPU (Apple, Arm Mali, Qualcomm Adreno, Imagination PowerVR) використовує плиткове відкладене рендерингу (TBDR): екран розбивається на маленькі плитки, і геометрія та шейдинг кожної плитки повністю обробляються у швидкій пам'яті на чіпі перед одноразовим записом у основну пам'ять. Ось чому деякі звички, вироблені на десктопі, активно шкодять продуктивності на мобільних:
- Зчитування з framebuffer посеред кадру (наприклад, ручний depth pre-pass з подальшим повноекранним зчитуванням) змушує до раннього скидання плитки, що набагато дорожче на TBDR-апаратурі, ніж на десктопному рендерері з негайним режимом.
- Уніфікована пам'ять означає, що GPU і CPU ділять ту саму фізичну RAM — немає окремого бюджету VRAM, на який можна покластися, і великі текстури безпосередньо конкурують з рештою пам'яті сторінки.
- Немає запасу для стійкої частоти. Мобільні SoC розраховані на набагато нижчу термальну межу, ніж десктопний GPU з активним охолодженням — див. розділ 5.
2. Обмежте devicePixelRatio
Сучасний телефон повідомляє devicePixelRatio
2.6-3+. Рендеринг canvas WebGL з таким повним співвідношенням
означає, що кожен фрагментний шейдер виконується приблизно в
9 разів частіше, ніж при 1x — заради покращення чіткості, яке
ледь помітне на маленькому екрані, розташованому на відстані
витягнутої руки.
const pixelRatio = Math.min(window.devicePixelRatio, 1.5); // 1.5-2 — хороше значення для мобільних за замовчуванням
renderer.setPixelRatio(pixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
Для симуляцій, важких за fill-rate (системи частинок, симуляції рідин, будь-що з великими повноекранними проходами шейдерів), розгляньте ще агресивніший підхід: рендерити внутрішньо у частці від CSS-розміру і дозволити canvas масштабуватися через CSS, що коштує майже нічого порівняно з шейдингом кожного пікселя в нативному розширенні.
// Внутрішнє розширення рендеру 0.75x, масштабоване браузером через CSS
const renderScale = 0.75;
renderer.setSize(
window.innerWidth * renderScale,
window.innerHeight * renderScale,
false // false = не чіпати розмір CSS, лише буфер малювання
);
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';
3. Точність шейдера: уникайте переповнення highp
Кваліфікатори точності GLSL ES (lowp,
mediump, highp) на мобільних — не
просто рекомендації: багато мобільних GPU виконують математику
фрагментного шейдера mediump на дійсно вужчих
апаратних шляхах, ніж highp, а деякі старіші
мобільні GPU взагалі не підтримують highp у
фрагментному шейдері (специфікація дозволяє тихо переходити на
mediump).
| Кваліфікатор | Типовий діапазон | Використовувати для |
|---|---|---|
lowp |
~256 окремих значень, ±2 | Нормалізовані кольори, прості коефіцієнти 0-1 |
mediump |
~16-бітний діапазон float | UV-координати, більшість математики освітлення, точність фрагментного шейдера за замовчуванням на багатьох GPU |
highp |
~32-бітний діапазон float | Позиції у світовому просторі далеко від початку координат, точне накопичення часу |
Найпоширеніший тихий баг, специфічний лише для мобільних:
симуляція, що накопичує пройдений час в uniform-змінній
mediump (або з відкатом від highp),
через кілька хвилин починає показувати помітне тремтіння або
смуги, коли накопичене значення перевищує точність
mediump при такій величині — на десктопі з повною
підтримкою highp все працює добре, і ламається
лише на телефоні в руках ваших користувачів.
// Періодично скидайте uniform-змінні на основі часу замість вічного накопичення
const t = (performance.now() % 100000) / 1000; // обгортається кожні ~100с
material.uniforms.uTime.value = t;
gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT)
— якщо .precision дорівнює 0, GPU не має справжньої
підтримки highp у фрагментному шейдері й тихо
виконує вашу «highp» математику як mediump.
4. Бюджети draw call і текстурної пам'яті
Орієнтовні, консервативні початкові бюджети для телефону середнього класу (Android 2-3-річної давності або старіший iPhone) порівняно з типовим десктопом:
| Бюджет | Десктоп (типово) | Мобільний середнього класу |
|---|---|---|
| Draw call / кадр при 60fps | ~1,000-3,000+ | ~150-400 |
| Трикутники / кадр | Кілька мільйонів | ~200 тис.-500 тис. |
| Загальна текстурна пам'ять | Кілька ГБ | ~150-400 МБ (спільно з ОС і браузером) |
- Групуйте геометрію / використовуйте InstancedMesh — об'єднання статичних мешів або інстансинг повторюваних об'єктів (частинки, дерева, комірки сітки) напряму скорочує draw call — найтісніший бюджет на мобільних.
-
Стиснені текстури — постачайте текстури
KTX2/Basis Universal (транскодовані в ASTC на сучасних мобільних GPU, ETC2 як відкат) замість сирих PNG/JPEG, декодованих у повний RGBA8 у пам'яті GPU; це саме по собі може скоротити VRAM текстур у 4-8 разів. - Мипмапи завжди — зменшені текстури без мипмап сильно б'ють по пропускній здатності кешу текстур на мобільних; завжди генеруйте мипмапи для всього, що переглядається з різної відстані.
5. Термальний тротлінг
У телефона немає активного охолодження. Тривале високе навантаження на GPU нагріває SoC, доки термальне керування ОС не знизить частоту GPU (і часто CPU) — іноді на 30-50% нижче пікової — щоб залишитися в безпечних температурах. Це означає:
- Симуляція, що показує плавні 60fps у перші 30 секунд, може впасти до 35-40fps після 3-5 хвилин безперервної гри, без жодної зміни коду і без дій користувача.
- Тротлінг нелінійний і специфічний для пристрою — флагманський телефон з паровою камерою переносить набагато більше стійке навантаження, ніж бюджетний телефон у тонкому пластиковому корпусі, навіть за схожих пікових результатів бенчмарку.
// Мінімальний регулятор адаптивної якості
let quality = 1.0;
function animate(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
if (dt > 1 / 45) quality = Math.max(0.5, quality - 0.02); // відстаємо → знижуємо
else if (dt < 1 / 58) quality = Math.min(1.0, quality + 0.005); // є запас → повільно піднімаємо
particleSystem.setActiveCount(Math.floor(maxParticles * quality));
requestAnimationFrame(animate);
}
6. Тач-введення замість миші/клавіатури
Кожен десктоп-орієнтований патерн взаємодії потребує тач-дружного еквіваленту — немає hover, немає правого кліку, і часто взагалі немає клавіатури:
| Десктопний патерн | Мобільно-дружний еквівалент |
|---|---|
| Hover для превью/підсвічування | Тап для вибору з видимим станом «обрано» (hover не існує) |
| Контекстне меню правого кліку | Жест довгого натискання або постійна екранна кнопка |
| Перетягування мишею для обертання камери | Перетягування одним пальцем для обертання, щипок двома пальцями для масштабування, перетягування двома пальцями для панорамування |
| Колесо миші для масштабування | Жест щипка (touchmove з двома активними точками дотику) |
| Клавіатурне керування WASD/стрілками | Екранний віртуальний джойстик або кнопки напрямку |
// Мінімальне відстеження відстані для щипка-масштабування
let lastPinchDist = null;
canvas.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
const [a, b] = e.touches;
const dist = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
if (lastPinchDist !== null) {
const delta = dist - lastPinchDist;
camera.position.multiplyScalar(1 - delta * 0.003); // масштаб через зміну відстані
}
lastPinchDist = dist;
e.preventDefault(); // зупиняє прокрутку/масштабування сторінки
}
}, { passive: false });
canvas.addEventListener('touchend', () => { lastPinchDist = null; });
touch-action: none на
елементі canvas у CSS разом зі слухачами
{ passive: false } — інакше власні жести
щипка-масштабування/прокрутки браузера конфліктуватимуть з
вашою обробкою дотику.
7. Тестування на реальних пристроях
Емульований «мобільний» режим Chrome DevTools (панель пристрою) лише симулює розмір екрана й тач-події — шейдери все одно виконуються на драйвері десктопного GPU, що не дає корисного сигналу про реальну продуктивність мобільного GPU, відкати точності чи термальну поведінку.
-
Підключіть Android-телефон через USB з увімкненим
USB-налагодженням, потім відкрийте
chrome://inspectна десктопі — він перелічує відкриті вкладки телефону з живим посиланням «inspect» у справжні DevTools, що працюють проти реального пристрою. - На iOS підключіть iPhone/iPad через кабель, увімкніть Web Inspector у Налаштування → Safari → Додатково, потім відкрийте меню Develop у Safari на Mac, щоб приєднатися до сторінки, що виконується на пристрої.
- Тестуйте на найстарішому пристрої, з якого ваша аналітика показує значущий трафік, а не на власному найновішому телефоні — результат 60fps на актуальному флагмані нічого не говорить про 3-річні Android-пристрої середнього класу, які зазвичай є справжнім довгим хвостом.
chrome://gpu на самому
підключеному пристрої, або з
туторіалом з
3D-оптимізації для технік LOD і відсікання, що скорочують
ті самі бюджети draw call і трикутників, обговорені в розділі 4.
Часті запитання
Чого я навчуся в цьому уроці?
Випустіть WebGL-симуляцію, яка дійсно добре працює на телефонах: обмеження pixel ratio, точність шейдерів, бюджети draw call і текстурної пам'яті, термальний тротлінг, тач-керування та мобільні баги, яких немає на десктопі.
Які теми розглядаються в цьому уроці?
Цей урок охоплює такі теми: Інший режим, а не менший десктопний GPU, Обмежте devicePixelRatio, Точність шейдера: уникайте переповнення highp, Бюджети draw call і текстурної пам'яті, Термальний тротлінг, Тач-введення замість миші/клавіатури, Тестування на реальних пристроях.
Скільки часу займає цей урок?
Цей урок займає приблизно 25 хв.
Які попередні знання потрібні?
Це урок рівня «Середній рівень» — окрема попередня підготовка, крім базового JavaScript, не потрібна.