Event loop и рендер в браузере, часть 1


Представьте себе, что вы работаете в компании, и ваша работа — общаться и информировать клиентов. Вы появляетесь на рабочем месте, включаете компьютер и начинаете ждать. Вы в компании единственный, кто общается с клиентами, поэтому все входящие заявки приходят вам.

К вам сразу же клиенты начинают звонить по телефону. Ваша задача:

  1. Ответить им, пообщаться, понять их вопрос;

  2. Затем внести клиента в ваш список, найти ответ на вопрос и сообщить клиенту ответ, завершить разговор;

  3. От некоторых клиентов (не от всех) нужно отправить полученную информацию по почте внутрь компании, чтобы с ней проложили работать коллеги.

Таким образом у вас есть всегда некая очередь входящих звонков и очередь писем на отправку. Если звонит телефон, то вы отвлекаетесь от почты, и отвечаете по телефону. Если телефон молчит, то вы переходите к отправке почты. Соответсвенно, если телефонные звонки длинные, то очередь звонков копится, и вы не успеваете отправлять почту. Или наоборот, если вам нужно отправлять письма очень часто или они большие, то вы будете с задержками отвечать по телефону.

Event Loop

А теперь представьте, что в браузере есть тоже некий «исполнитель», который на сайте постоянно ждёт задачи на исполнение — такой бесконечный цикл, который называется Event Loop. Он делает все задачи по исполнению JS-кода (Task) и отрисовке сайта (Render). Этот исполнитель один, и работает он в одном «потоке», так как задачи по исполнению JS-кода и отрисовке DOM взаимосвязаны и их проблематично разделять. Event loop связан с обновлением экрана — в нём рассчитываются кадры страницы.

Все JS-задачи, которые поступают на исполнение, ставятся в очередь — TaskQueue. Задачи по рендеру тоже записываются в очередь — RenderQueue. Вспомните пример со звонками и почтой. Исполнитель делает поступающие JS-задачи (это очередь телефонных звонков), а когда освобождается от них, переходит к задачам по отрисовке интерфейса (отправка почты).

В приоритете — работа с JS-задачами, а когда приходит необходимость отрисовать следующий кадр для обновления экрана, Event Loop переходит к задачам отрисовки.

Как определяется эта необходимость? Современные устройства работают с частотой отрисовки 60 кадров в секунду (FPS). Это значит, что каждые 16.6мс (1/60 секунды) браузеру нужно перерисовывать следующий кадр.

В случае с общением нашего менеджера с клиентами по телефону, длинные звонки «задерживают» отправку почты. Так же и длинные JS-задачи (>16.6мс) заставляют браузер откладывать отрисовку. Поэтому в момент, когда отрисовка уже была нужна, браузер может «пропустить» кадр. То есть частота обновления кадров станет уже <60FPS. Визуально интерфейс начнёт «подтормаживать» или вообще «фризиться».

Аналогично и со случаем, когда много писем в очереди могут помешать менеджеру вовремя отвечать на звонки. Если в RenderQueue задач по отрисовке много или они сложные, то исполнение JS-задач в TaskQueue тоже может задерживаться, что проявляется в «тормозах».

Микротаски

Есть ещё одна разновидность задач — микротаски (MicroTask). Это такие особенные JS-задачи — колбеки для Promise или mutationObserver. Микротаски собираются в отдельную очередь — MicroTaskQueue. Чем микротаски отличаются от обычных тасков? Они исполняются сразу же, когда дорабатывает текущий таск или микротаск, то есть вне очереди TaskQueue и RenderQueue. Эта особенность может быть критична в случае, когда микротаски циклично вызывают следующие микротаски, откладывая рендер и обычные JS-таски, так как до них просто не доходит очередь.