Практика: задача с бесконечным счетчиком

Вопрос: что будет происходить в браузере, если запустить этот код?
 
Задание 1
<html> <head> <title>Task 1</title> </head> <body> <div id="container">0</div> <script> document.addEventListener("DOMContentLoaded", () => { const hostEl = document.getElementById("container"); let counter = 0; while (true) { hostEl.innerText = counter++; } }); </script> </body> </html>
Задание 2
<html> <head> <title>Task 2</title> </head> <body> <div id="container">0</div> <script> document.addEventListener("DOMContentLoaded", () => { const hostEl = document.getElementById("container"); let counter = 0; while (true) { setTimeout(() => { hostEl.innerText = counter++; }, 10); } }); </script> </body> </html>
Задание 3
<html> <head> <title>Task 3</title> </head> <body> <div id="container">0</div> <script> document.addEventListener('DOMContentLoaded', () => { const hostEl = document.getElementById('container'); let counter = 0; const func = () => { setTimeout(() => { hostEl.innerText = counter++; func(); }, 10) }; func(); }) </script> </body> </html>
Задание 4
<html> <head> <title>Task 4</title> </head> <body> <div id="container"> 0 </div> <script> document.addEventListener('DOMContentLoaded', () => { const hostEl = document.getElementById('container'); let counter = 0; const func = () => { new Promise(res => { hostEl.innerText = counter++; res(); }) .then(() => func()); }; func(); }); </script> </body> </html>
 
🎯 Зачем спрашивают
  • Проверить понимание модели выполнения JS (event loop) и различий между:
    • синхронным и асинхронным кодом
    • макро- и микрозадачами,
    • временем рендеринга браузера.
  • Проверить, умеет ли кандидат объяснить, почему UI блокируется и как этого избежать.
  • Понять, насколько глубоко кандидат понимает взаимодействие JS и браузера (critical rendering path, render blocking).
 
📝 Ответ
Коротко:
Задача 1
<html> <head> <title>Task 1</title> </head> <body> <div id="container">0</div> <script> document.addEventListener("DOMContentLoaded", () => { const hostEl = document.getElementById("container"); let counter = 0; while (true) { hostEl.innerText = counter++; } }); </script> </body> </html>
Скрипт заблокирует отрисовку UI из-за бесконечного while цикла. Даже 0 не отобразится.
Задача 2
<html> <head> <title>Task 2</title> </head> <body> <div id="container">0</div> <script> document.addEventListener("DOMContentLoaded", () => { const hostEl = document.getElementById("container"); let counter = 0; while (true) { setTimeout(() => { hostEl.innerText = counter++; }, 10); } }); </script> </body> </html>
Скрипт заблокирует отрисовку UI из-за бесконечного while цикла. Даже 0 не отобразится. setTimeout не решит проблему, так как вызывается внутри бесконечного цикла.
 
 
Задача 3
<html> <head> <title>Task 3</title> </head> <body> <div id="container">0</div> <script> document.addEventListener('DOMContentLoaded', () => { const hostEl = document.getElementById('container'); let counter = 0; const func = () => { setTimeout(() => { hostEl.innerText = counter++; func(); }, 10) }; func(); }) </script> </body> </html>
Счетчик будет обновляться и код не приведет к ошибке. Каждый setTimeout создаёт новый таймер, но предыдущий уже отработал к моменту создания нового, поэтому таймеры не копятся.
Рекурсия здесь не на стеке JavaScript, потому что она асинхронная. Каждый вызов func() происходит через Event Loop после завершения предыдущего. Поэтому стек вызовов не переполнится.
 
 
 
Задача 4
<html> <head> <title>Task 4</title> </head> <body> <div id="container"> 0 </div> <script> document.addEventListener('DOMContentLoaded', () => { const hostEl = document.getElementById('container'); let counter = 0; const func = () => { new Promise(res => { hostEl.innerText = counter++; res(); }) .then(() => func()); }; func(); }); </script> </body> </html>
Скрипт заблокирует отрисовку UI. Каждая итерация делает
new Promise(res => { hostEl.innerText = counter++; res(); }) res() вызывается синхронно, поэтому .then(() => func()) ставит микрозадачу.
Обработчик .then запускается в микроочереди. Внутри он снова вызывает func(), которая тут же создаёт новый Promise и снова синхронно резолвит его, добавляя ещё одну микрозадачу.
По спецификации микрозадачи выполняются до опустошения очереди, прежде чем браузер перейдёт к следующей макрозадаче (рендер/таймеры/IO). А тут очередь никогда не пустеет: каждая микрозадача добавляет следующую.
Поэтому будет 100% загрузка потока, рендер не произойдет (даже изменения innerText не успеют нарисоваться).
 
 
⚖️ Компромиссы
 
🔎 Встречные вопросы
  • Почему setTimeout не спасает в задаче №2?
  • Чем микрозадачи отличаются от макрозадач?
  • Почему Promiseрекурсия “вешает” вкладку?
  • Как сделать счётчик, который не блокирует UI?
  • Когда лучше использовать requestAnimationFrame вместо таймера?
 
🚩 Красные флаги
  • Ответы вроде «всё будет работать» без упоминания event loop.
  • Путаница между макро и микрозадачами.
  • Упоминание «setTimeout всегда решает проблему блокировки».
  • Нет объяснения, почему рендер не происходит.
 
🛠 Практика
 
📚 Источники / ссылки
  • YouTubeYouTubeWhat the heck is the event loop anyway? | Philip Roberts | JSConf EU
  • MDN Web DocsMDN Web DocsJavaScript execution model - JavaScript | MDN
  • jaffathecakejaffathecakeTasks, microtasks, queues and schedules