Skip to main content

Сучасна архітектура браузера (процеси та потоки)

Сучасна архітектура браузера - це багатопроцесна система, де browser process координує ізольовані renderer processes (по одному на сайт або таб) та службові процеси для мережі й графіки, а потоки виконують завдання всередині кожного процесу.

Теорія

TL;DR

  • Браузер розбитий на окремі OS-процеси: browser process (координатор), renderer process на сайт/таб, GPU process, network process
  • Однопроцесні браузери (епоха IE6): збій одного таба вбиває все. Chrome з 2008: збій залишається всередині того renderer
  • Кожен renderer запускає кілька потоків: main thread (JS + DOM), compositor thread (скролінг на 60fps), raster threads, worker threads
  • Site Isolation (Chrome 67+) розміщує кожен origin у власному renderer process - це була пряма відповідь на Spectre/Meltdown
  • Main thread блокує рендеринг коли зайнятий; важку роботу передавай у Web Workers

Швидкий приклад

javascript
// Main thread renderer - якщо заморозиш його, зависне лише ЦЕЙ таб while (true) {} // інші таби продовжують працювати у своїх процесах // Рішення: worker thread (той самий renderer process, окремий потік) const worker = new Worker('heavy.js'); worker.postMessage({ task: 'compute' }); worker.onmessage = (e) => console.log(e.data); // Комунікація між табами йде через IPC browser process const bc = new BroadcastChannel('app'); bc.postMessage({ type: 'update' }); // копіюється, не ділиться напряму

Після Spectre, SharedArrayBuffer потребує заголовків COOP/COEP. Без них прямий обмін пам'яттю між renderer processes недоступний - тільки копіювання.

Карта процесів

Browser process - координатор. Він запускає всі інші процеси, управляє дозволами, відповідає за адресний рядок та lifecycle всього браузера.

Renderer processes обробляють вміст сторінок. Chrome за замовчуванням запускає один на origin сайту (Site Isolation). Кожен renderer ізольований на рівні OS: не може напряму звернутись до диску чи мережі - тільки через IPC до browser process. Chromium використовує для цього Mojo.

GPU process знаходиться між усіма renderer-ами та відеокартою. Кожен renderer передає намальовані шари туди, GPU process їх компонує. Тому збій GPU виглядає інакше ніж збій renderer.

Network process (ізольований з Chrome 88) відповідає за HTTP, DNS, TLS, куки та CORS. До цього networking жив у browser process - баг у TLS міг дістатись до головного координатора.

Головна різниця

Однопроцесні браузери ділили один адресний простір між усіма табами. Exploit у пам'яті одного таба міг читати дані з будь-якого іншого. Багатопроцесна ізоляція дає кожному renderer власну пам'ять, а пісочниця OS блокує прямий доступ до файлів та мережі. Ціна реальна: кожен renderer process додає приблизно 100 МБ overhead. Тому Chrome має ліміт на кількість renderer processes і об'єднує таби одного сайту при зростанні memory pressure.

Коли що використовувати

  • Важкі обчислення в JS -> Web Worker (main thread залишається вільним для рендерингу)
  • Плавні анімації -> тільки transform та opacity (compositor thread, не торкається main thread)
  • Комунікація між табами -> BroadcastChannel або postMessage (не SharedArrayBuffer без COOP/COEP заголовків)
  • Дебаг збоїв renderer -> chrome://crashes/
  • Профілювання міжпроцесної продуктивності -> chrome://tracing
  • Проблеми з пам'яттю при багатьох табах -> chrome://memory-internals

Порівняння браузерів

БраузерАрхітектураSite IsolationRenderer processes
ChromeБагатопроцеснаПовна (Chrome 67+)Один на origin сайту
EdgeБагатопроцесна (Chromium)ПовнаОдин на origin сайту
FirefoxБагатопроцесна (e10s, з v54)ЧастковаДо 8 за замовчуванням
SafariБагатопроцесна (WebKit2, з 2010)ЧастковаОдин на групу табів

Як це працює всередині

Коли ти відкриваєш таб, browser process надсилає URL до network process. Щойно приходять заголовки відповіді, він вирішує який renderer process обробить сторінку: новий для cross-site навігації, той самий для однакового сайту. Renderer запускає Blink (движок HTML/CSS) та V8 (движок JS) разом. Blink передає команди малювання до compositor thread. Compositor розбиває сторінку на шари та відправляє їх до GPU process через Mojo IPC.

Саме тому анімації через transform не торкаються main thread - вони живуть у ланцюжку compositor-GPU. V8 має один heap на renderer process, тому два таби google.com можуть ділити renderer, але не мають прямого доступу до JS-змінних один одного - кожен контекст ізольований всередині V8.

Типові помилки

Блокування main thread довгим синхронним циклом:

javascript
// Неправильно - зависає весь таб: UI, події, рендеринг for (let i = 0; i < 1_000_000_000; i++) { /* робота */ } // Виправлення - дроби через requestAnimationFrame для візуальної роботи function processChunk(i) { if (i >= 1_000_000_000) return; // частина роботи requestAnimationFrame(() => processChunk(i + 10_000)); } // Або Web Worker для чистих обчислень const worker = new Worker('compute.js'); worker.postMessage({ start: 0, end: 1_000_000_000 });

Очікування що SharedArrayBuffer працює без заголовків:

javascript
// Неправильно - SecurityError в Chrome 68+ без заголовків ізоляції const sab = new SharedArrayBuffer(1024); window.postMessage(sab, '*'); // Виправлення - додай до відповіді сервера: // Cross-Origin-Opener-Policy: same-origin // Cross-Origin-Embedder-Policy: require-corp

Анімація властивостей що запускають layout на кожному кадрі:

css
/* Неправильно - примусовий layout на main thread кожного кадру */ .box { left: 0; animation: move 1s; } @keyframes move { to { left: 100px; } } /* Правильно - compositor обробляє без main thread */ .box { transform: translateX(0); animation: move 1s; } @keyframes move { to { transform: translateX(100px); } }

Особисто витратив цілий день на дебаг анімації в React-додатку яка дропала фрейми. DevTools Performance показував paint на main thread кожного кадру. Причина - анімувалась властивість width замість transform. Заміна на transform: scaleX() вирішила проблему одразу.

Очікування що console.log покриє всі процеси:

Логи ізольовані по renderer. Якщо дебажиш Service Worker, його вивід іде в окремий DevTools контекст. Для міжпроцесного профілювання - chrome://tracing з категорією disabled-by-default-devtools.timeline.

Де зустрічається в реальних проектах

  • Chrome/Edge -> кожен таб з іншого домену в окремому renderer; Puppeteer для E2E-тестів запускає повний browser instance, кожен newPage() отримує свій renderer
  • Firefox -> Electrolysis (e10s) дає до 8 content processes; кожен обробляє групу табів
  • React/Vue -> virtual scroll зі 100k рядків потребує Web Workers щоб скролінг залишався плавним поки дані обробляються у фоні
  • Node.js cluster -> cluster.fork() дзеркалює те як Chrome запускає renderer processes: батьківський процес координує, воркери обробляють запити
  • PWA/Service Workers -> Service Workers працюють в окремому потоці всередині renderer process і проксують мережеві запити без блокування JS сторінки

Follow-up питання

Q: Чому Chrome обрав багатопроцесність замість багатопоточності для ізоляції табів?
A: Потоки ділять один адресний простір, тому баг в одному може зіпсувати дані іншого. Окремі процеси отримують ізоляцію пам'яті на рівні OS та власні sandbox-політики. Ціна - вищі витрати на IPC, для чого Chromium і розробив Mojo.

Q: Що відбувається на пристрої з малою пам'яттю при 20 відкритих табах?
A: Browser process відстежує memory pressure і починає об'єднувати таби одного сайту в спільний renderer process. Також відкидає renderer фонових табів - при поверненні побачиш "Aw, Snap!". Прапорець --max-renderer-process-count=4 встановлює жорсткий ліміт.

Q: Як V8 вписується в модель процесів?
A: Кожен renderer process отримує один екземпляр V8. V8 працює на main thread, але може запускати внутрішні worker threads для фонової компіляції. JS heap повністю ізольований на renderer, тому об'єкти між табами не можна ділити напряму без копіювання.

Q: Chrome 88 переніс мережевий стек в окремий процес. Що це змінило для розробників?
A: До цього баг у обробці TLS міг вплинути на browser process напряму. Після розділення network process ізольований окремо. Для PWA це означало що fetch() з Service Workers тепер іде через network process навіть коли renderer неактивний - менше зайвих wake-ups, краща автономність.

Q: Намалюй модель процесів для двох табів: google.com та example.com.
A: Browser process запускає: GPU process, Network process, Renderer1 (origin google.com), Renderer2 (origin example.com). Обидва renderer-и спілкуються з browser process через Mojo IPC. Жоден renderer не може звернутись до іншого напряму.

Приклади

Передача важкого обчислення у Web Worker

javascript
// main.js - виконується на main thread renderer const worker = new Worker('worker.js'); worker.postMessage({ numbers: Array.from({ length: 1_000_000 }, (_, i) => i) }); worker.onmessage = (event) => { console.log('Сума:', event.data.result); // UI залишався responsive весь час }; // worker.js - окремий потік, той самий renderer process self.onmessage = (event) => { const sum = event.data.numbers.reduce((acc, n) => acc + n, 0); self.postMessage({ result: sum }); };

Обчислення виконуються у worker thread всередині того самого renderer process. Main thread не зупиняється, тому сторінка продовжує реагувати на дії користувача поки йде робота у фоні.

Комунікація між табами через BroadcastChannel

javascript
// tab-a.js (Renderer Process 1 - google.com) const channel = new BroadcastChannel('notifications'); channel.postMessage({ type: 'NEW_MESSAGE', id: 42 }); // Шлях: renderer1 -> IPC browser process -> renderer2 // tab-b.js (Renderer Process 2 - той самий origin google.com) const channel = new BroadcastChannel('notifications'); channel.onmessage = (event) => { console.log('Отримано:', event.data); // { type: 'NEW_MESSAGE', id: 42 } // Прямого ділення пам'яті між процесами немає - повідомлення скопійовано };

Після Spectre пряме ділення пам'яті між renderer processes потребує явного opt-in через COOP та COEP заголовки. BroadcastChannel копіює повідомлення через browser process і тому працює без них. Для невеликих даних overhead копіювання незначний.

Анімація на compositor thread без участі main thread

html
<style> .spinner { width: 40px; height: 40px; background: #4285f4; /* transform та opacity - дві властивості якими compositor керує незалежно від main thread */ animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } </style> <div class="spinner"></div>

Навіть якщо main thread зайнятий парсингом великого JSON, ця анімація тримає 60fps. Compositor thread має власну копію шару і обертає його без жодного звернення до main thread.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?