РозробкаБудь-який рівень

JavaScript

JavaScript — мова програмування, яку виконує кожен браузер: один потік на сторінку, один стек викликів і черга для всього, що мусить чекати. Більшість того, що здається в ній довільним, випливає з цієї моделі або з рішення перших років мови, яке веб уже не може скасувати. Сторінка починається з дванадцяти несподіванок — кожна з правилом, що за нею стоїть, і виправленням; далі — цикл подій, де асинхронний код губить час і помилки, числа й посилання, область видимості, модулі й дерево залежностей і п’ять задокументованих кейсів.

Потоків
Один на сторінку, плюс воркери
Стандарт
ECMAScript 2026 · щороку
Найбільше точне ціле
2^53 − 1

Дванадцять несподіванок і правило за кожною

Спершу таблиця, бо саме через це найчастіше відкривають сторінку про JavaScript: мова щойно зробила щось, схоже на баг. Кожен рядок — це те, що ви бачите, правило, яке це спричиняє, і виправлення. Жоден із дванадцяти не є багом. Кожен — це правило, записане в специфікації, яке зустріло код людини, що з ним ще не стикалася, — а правил так мало, що решта сторінки охоплює кожне з них.

Що ви бачитеЩо насправді відбуваєтьсяВиправлення
1. Колбек промісу виконується раніше за setTimeout(fn, 0), поставлений до ньогоКолбеки промісів ідуть у чергу мікрозадач, а її вичерпують повністю, перш ніж узагалі глянути на наступний таймер.Ніколи не впорядковуйте роботу таймером. Чекайте через await саме те, від чого залежите.
2. Сторінка замерзає, поки йде обчисленняПотік один. Поки ваша функція в стеку, не виконується нічого іншого — ні обробник кліку, ні відмальовка.Поділіть роботу на шматки й віддавайте керування між ними — або винесіть її у Web Worker.
3. Три await поспіль тривають стільки, скільки всі три разомКожен await завершується раніше, ніж наступний запит узагалі стартує.Запускайте незалежні запити разом і чекайте Promise.all.
4. forEach з async-колбеком не чекаєforEach ігнорує те, що повертає колбек, — тут це проміс — і одразу йде далі.for…of з await усередині або await Promise.all(items.map(…)).
5. try / catch не ловить помилкуПомилку кинуто пізніше, на іншому оберті циклу, коли блок try уже завершився.Робіть await усередині try або чіпляйте .catch() там, де віддаєте проміс.
6. 0.1 + 0.2 дорівнює 0.30000000000000004Кожне число — це 64-бітне двійкове число з рухомою комою, а 0,1 не має точного двійкового запису.Рахуйте гроші в цілих дрібних одиницях (центах, копійках); округлюйте лише для показу.
7. ID із сервера повертається з іншими останніми цифрамиЦілі понад 2^53 − 1 неможливо зберегти точно, і JSON.parse мовчки їх округлює.Передавайте великі ID рядками. Для арифметики над ними — BigInt.
8. Зміна копії змінює оригіналРозкладання й Object.assign копіюють один рівень. Вкладені об’єкти лишаються спільними.structuredClone() для глибокої копії — або копіюйте кожен рівень, який змінюєте.
9. Чинний 0 чи '' підміняється значенням за замовчуванням|| вважає відсутнім кожне хибне значення — зокрема 0, порожній рядок і false.?? для значень за замовчуванням і ?. для необов’язкового доступу: обидва означають «null або undefined» — і нічого більше.
10. Колбеки, створені в циклі, бачать лише останнє значенняЗ var уся функція має одне зв’язування, і кожен колбек замикається саме на ньому.let або const: кожна ітерація циклу отримує власне зв’язування.
11. [10, 9, 1].sort() повертає [1, 10, 9]Без функції порівняння sort перетворює значення на рядки й порівнює їх символ за символом.(a, b) => a - b для чисел, Intl.Collator для слів, toSorted() — щоб не чіпати оригінал.
12. Дата показується на день ранішеnew Date('2026-09-30') — це північ за UTC, а в Нью-Йорку тоді ще 29 вересня.Зберігайте календарну дату як дату, а не як момент часу: Temporal.PlainDate або сам рядок.

Один потік, дві черги

JavaScript у вкладці браузера виконується в одному потоці. Стек викликів один — це список функцій, що виконуються зараз, і кожна чекає, доки поверне та, що над нею, — і поки в ньому хоч щось є, у цій вкладці не виконується більше нічого. Ні інший обробник, ні колбек таймера, ні відмальовка. Саме останнє й означає «заблокувати інтерфейс» буквально: браузер не може намалювати кадр, бо ваш код іще не повернувся. Це рядок 2 таблиці.

JavaScript виконується в одному потоці з одним стеком викликів. Коли стек порожніє, цикл вичерпує всю чергу мікрозадач — кожен колбек промісу, зокрема ті, що додалися під час вичерпування, — потім бере рівно одну макрозадачу, а потім дає браузеру намалювати кадр. Саме ця асиметрія пояснює, чому колбек промісу виконується раніше за раніше поставлений setTimeout і чому нескінченний ланцюг промісів вішає сторінку, а нескінченний ланцюг таймаутів — ні.

Коли стек порожніє, керування бере цикл подій, і він поводиться з двома чергами по-різному. Він повністю вичерпує чергу мікрозадач — кожен колбек промісу й усі нові мікрозадачі, які ці колбеки додають під час вичерпування. Потім бере рівно одну макрозадачу: один колбек таймера, один обробник кліку, одну мережеву відповідь. Потім браузер може намалювати кадр — і оберт починається знову. Ця асиметрія — увесь рядок 1. Колбек промісу не був швидшим за таймер; він стояв у черзі, яку завжди вичерпують першою.

Де код чекаєЩо туди потрапляєКоли виконуєтьсяПастка
Стек викликівЗвичайний синхронний кодЗараз і до кінцяНічого не малюється, доки він не поверне (рядок 2)
Черга мікрозадачКолбеки промісів, код після кожного await, queueMicrotaskУсі, щойно стек порожнієЛанцюг, що весь час додає нові, морить сторінку назавжди
Черга задач (макрозадачі)Таймери, події користувача, мережеві відповіді, postMessageОдна за оберт циклуВкладені таймери обмежено щонайменше 4 мс, тож 0 — це прохання, а не обіцянка
Кадр анімаціїКолбеки requestAnimationFrameБезпосередньо перед наступною відмальовкоюВажка робота тут затримує саме той кадр, який мала намалювати
Web WorkerОкремий скрипт із власним цикломПаралельно, в іншому потоціБез DOM, а дані переходять лише скопійованими повідомленнями

Рядок 2 має своє число. Браузер вважає довгою задачею будь-яку, що тримає потік понад 50 мілісекунд, а з березня 2024 року Google вимірює чуйність як INP — Interaction to Next Paint, один із трьох , — де 200 мілісекунд від кліку до наступного кадру вважаються добрим результатом. Цикл, що 400 мілісекунд сортує список, провалює це сам-один, хай як його написано — з async чи без. await нічого не переносить в інший потік: він призупиняє вашу функцію й дає циклу працювати далі, але коли очікуване значення надходить, код після нього виконується в тому самому єдиному потоці.

Де асинхронний код губить час і помилки

Асинхронний код мав три покоління синтаксису, і всі три досі трапляються в реальних проєктах — часто в одному файлі. Під ними той самий механізм: проміс — це об’єкт, що заміняє результат, який ще не готовий, а async і await — спосіб написати код на промісах так, щоб він читався згори донизу. Відрізняється між трьома те, як подорожують помилки і наскільки читабельний порядок подій.

СтильОбробка помилокДе досі доречний
КолбекиЗа домовленістю — помилка першим аргументом, і це ніщо не забезпечує.Обробники подій і старі API Node; ніколи для нової послідовної роботи.
Проміси.catch() покриває весь ланцюг вище — один обробник на багато кроків.Поєднання роботи, що йде одночасно: Promise.all та його родичі.
async / awaitЗвичайний try / catch і стектрейси, у яких названо вашу функцію.Типовий вибір для всього послідовного — читається в порядку подій.

Чистий синтаксис робить одну помилку легкою, і це рядок 3. Три незалежні запити, очікувані один за одним, тривають стільки, скільки всі три разом, бо другий не стартує, доки не завершиться перший. Запущені разом і очікувані через Promise.all, вони тривають стільки, скільки найповільніший. У коді ніщо не виглядає хибним: кожен рядок правильний, а сторінка все одно більш ніж удвічі повільніша, ніж могла б бути.

Три незалежні запити по 300, 200 і 400 мс. Очікувані один за одним, вони стартують лише після завершення попереднього, і результат готовий на 900 мс. Запущені разом і очікувані через Promise.all, вони закінчуються разом із найповільнішим — на 400 мс. Передані у forEach з async-колбеком, вони теж стартують разом, але forEach не чекає: код після циклу виконується на 0 мс, раніше за будь-який результат, і на запит, що впаде, ніхто не чекає. Тривалості ілюстративні.

Третій рядок діаграми — це рядок 4 таблиці, і він гірший, ніж просто повільний. forEach тричі викликає ваш async-колбек, отримує три проміси, викидає їх і одразу повертається. Рядок після циклу виконується раніше, ніж надійшов бодай один результат, — а якщо один із трьох запитів упаде, жоден код ніде не чекає, щоб про це дізнатися. for…of з await усередині дає свідомо послідовну версію, await Promise.all(items.map(…)) — версію «все разом».

КомбінаторЗавершується, колиДля чого
Promise.allУсі успішні — або відхиляється на першій невдачіКілька речей, потрібних наступному кроку, кожна з них
Promise.allSettledУсі завершилися, успішно чи ніДашборд із віджетів, де падіння одного не має гасити решту
Promise.raceПерший завершився, хай якТайм-аут: запит проти таймера
Promise.anyПерший успішнийТой самий файл із кількох дзеркал — хто відповість

Дві звички закривають більшу частину цього розділу. Перша: проміс, який ви створили, — це проміс, який ви чекаєте, повертаєте або ловите; правило лінтера, що позначає «завислі» проміси (у typescript-eslint воно зветься no-floating-promises), за хвилину знаходить кожне місце, де ви забули. Друга: усе, що користувач може покинути, має бути скасовуваним. Передавайте у fetch сигнал AbortController і скасовуйте його, коли компонент зникає або змінюється рядок пошуку. Сторінка про React показує, що буває без цього: старіша, повільніша відповідь приходить другою й затирає новішу.

Числа, посилання й відсутні значення

У JavaScript майже для всього є один вид чисел, і це 64-бітне двійкове число з рухомою комою — double за стандартом IEEE 754. Воно зберігає приблизно 15–17 значущих десяткових цифр і зберігає дроби у двійковій системі. Це й увесь рядок 6: 0,1 у двійковому записі — нескінченний дріб, як одна третя в десятковому, тож зберігається округленим, а сума двох округлених значень це округлення показує. Кожна мова, що використовує double, дає ту саму відповідь; просто в JavaScript більшість людей зустрічає це вперше.

Рядок 7 — той самий факт на іншому кінці шкали. Double зберігає цілі числа точно лише до 2^53 − 1, тобто до 9 007 199 254 740 991; це значення доступне як Number.MAX_SAFE_INTEGER. Далі проміжок між сусідніми числами, які можна записати, стає 2, потім 4, і непарні числа починають зникати: JSON.parse('{"id": 9007199254740993}').id повертає 9007199254740992 — без помилки й без попередження. Бази даних видають 64-бітні ID як щось звичайне, тож ця межа не теоретична. Twitter наштовхнувся на неї у 2010 році, і розділ із кейсами нижче розповідає як.

Що насправді лежить у змінній

Примітиви — числа, рядки, булеві значення, null, undefined, символи й bigint — копіюються при присвоєнні. Усе інше є об’єктом, і змінна не містить об’єкта: вона містить посилання на нього, адресу. Тож дві змінні можуть називати один і той самий об’єкт, і зміна через одну видима через іншу. Масиви й функції теж об’єкти, і тому сортування масиву всередині функції змінює масив того, хто її викликав.

Змінна тримає адресу об’єкта, а не сам об’єкт. Розкладання original у copy створює новий об’єкт верхнього рівня, але його поле user тримає ту саму адресу, що й в оригіналу, тож обидва ведуть до одного вкладеного об’єкта: після copy.user.city = 'Lviv' і original.user.city дорівнює 'Lviv'. structuredClone копіює кожен рівень, тож deep має власний вкладений об’єкт і зберігає 'Kyiv'.

Діаграма — це рядок 8. Розкладання об’єкта — { ...original } — створює новий об’єкт і копіює його верхній рівень, а однією з речей на верхньому рівні є посилання на вкладену адресу. Тож копія й оригінал тепер ділять її, і зміна copy.user.city з’являється в original.user.city. structuredClone(), вбудований в усі основні браузери з 2022 року, копіює кожен рівень і вміє працювати з датами, мапами, множинами та циклічними посиланнями. Він замінив старий трюк JSON.parse(JSON.stringify(x)), який мовчки викидав функції та undefined і перетворював кожну дату на рядок.

Посилання — це ще й причина, чому фреймворки порівнюють стан за тотожністю. React вирішує, чи змінився стан, питаючи, чи нове значення — те саме посилання, що й старе, тож push у масив на місці дає йому ту саму адресу, і екран не оновлюється. Виправлення має ту саму форму, що й рядок 8, лише навиворіт: створюйте новий об’єкт для кожного рівня, який змінюєте, а решту свідомо лишайте спільною.

Відсутнє, порожнє й хибне

У мові є два значення для «нічого» — undefined, яке дає незадана змінна чи відсутня властивість, і null, яке хтось записав навмисно, — і короткий список значень, що в умові вважаються хибними: false, 0, порожній рядок, NaN, null і undefined, а ще рідкісні -0 та 0n. Рядок 9 живе в проміжку між цими двома списками. Нульова кількість і порожнє ім’я — справжні відповіді, а || їх викидає.

Ви пишетеВважає відсутнімГодиться для
x || fallbackКожне хибне значення, зокрема 0 і ''Майже нічого — це рядок 9
x ?? fallbackЛише null і undefinedЗначення за замовчуванням для налаштувань і параметрів
x?.yЛише null і undefined — і зупиняється на нихЧитання даних, яких може не бути
x == nullЛише null і undefinedЄдине місце, де нестрога рівність ідіоматична

Щодо рівності правило коротке: використовуйте === і не думайте про це. Нестрога рівність застосовує таблицю перетворень, чиї результати можна обґрунтувати поодинці й неможливо передбачити разом, і жодна кодова база ніколи не стала кращою, спираючись на неї. Два винятки варто знати. NaN === NaN — хибне, бо «не число» не дорівнює нічому, включно із собою, тож перевіряйте через Number.isNaN(). А typeof null дорівнює 'object' — баг 1995 року, який так і лишився: пізнішу пропозицію його виправити відхилили, бо на нього вже спиралося забагато коду.

Область видимості, замикання і вбудоване, що не зміниться

Замикання — це функція, яка пам’ятає змінні місця, де її створили, і тримає їх живими стільки, скільки живе сама. Це механізм, завдяки якому колбеки працюють пізніше, існує приватний стан у модулі й працює кожен хук у React. І це рядок 10. З var змінна належить усій функції, тож десять створених у циклі колбеків замикаються на одній і тій самій змінній і всі бачать її кінцеве значення. З let кожна ітерація циклу отримує свіжу змінну, і той самий код робить те, чого від нього всі чекали.

Це й є практична причина, чому var зник із нового коду. Його область — функція, а не блок, і він піднімається, тож витікає з блоків if і for та існує — як undefined — ще до рядка, що його оголошує. let і const мають блокову область, а читання їх до оголошення — це помилка, а не мовчазне undefined. Типовим вибором має бути const: він забороняє перепризначати ім’я, а це саме та помилка, яку варто попереджати, — і водночас дозволяє змінювати об’єкт, на який ім’я вказує; знову рядок 8.

this визначається тим, як функцію викликали, а не тим, де її написали, тож передача obj.method як колбека губить obj, і this стає undefined. Стрілкові функції не мають власного this і беруть його з коду навколо, тому вони й стали типовим вибором для колбеків. Якщо ви пишете .bind(this) чи const self = this, ви обходите звичайну функцію, яка мала бути стрілковою.

Дві вбудовані функції, застиглі в першому році

Рядки 11 і 12 зовсім не про модель виконання. Це поведінка, яку мова отримала рано й не може змінити, бо перше правило вебу — оновлення браузера не має ламати робочий сайт. sort() без аргументу порівнює значення як рядки, тож 10 стає перед 9 рівно так, як «10» стоїть перед «9» у словнику. Date розбирає голу дату на кшталт '2026-09-30' як північ за , а дату з часом і без зсуву — як місцевий час, і потім друкує обидві в місцевому часовому поясі. Тож дата, обрана на ноутбуці в Києві, там показується правильно, а в колеги в Нью-Йорку — на день раніше.

Для обох є сучасні заміни, які стоять поряд зі старою поведінкою, а не змінюють її. toSorted() (2023) повертає відсортовану копію, а не переставляє масив на місці. Temporal, який став частиною стандарту мови у 2026 році й працює в Chrome і Firefox, — Safari ще наздоганяє, тож там допомагає поліфіл, — розводить те, що Date змішує: Temporal.PlainDate — це календарна дата без часового поясу, Temporal.Instant — мить на часовій осі, а Temporal.ZonedDateTime — мить у конкретному місці. День народження — це PlainDate. Час платежу — Instant.

Модулі, збірка і дерево залежностей

Модуль — це файл із власною областю видимості, який явно каже, що він бере з інших файлів і що пропонує їм. У JavaScript щодня використовуються дві системи модулів, і проєкт, що їх змішує, ловить помилки, які не мають сенсу, доки не знаєш, що систем дві.

ES-модуліCommonJS
Синтаксисimport / exportrequire() / module.exports
Залежності відоміДо виконання будь-якого коду — імпорти статичніЛише під час виконання — require це виклик функції
Невикористані експортиМожна довести, що не використовуються, і викинути з бандлаЗазвичай потрапляють у бандл усе одно
Де вона за замовчуваннямБраузери й кожен новий пакетСтаріший код і пакети Node.js

Між кодом, який ви пишете, і кодом, який виконує браузер, зазвичай стоїть збірка: складальник, що зводить сотні модулів у кілька файлів, компілятор, що перетворює TypeScript і новіший синтаксис на те, що розуміють цільові браузери, і мініфікатор, що скорочує кожне ім’я. З цього два наслідки. Помилка в продакшені вказує на рядок 1, стовпчик 48 213 файлу, якого ви ніколи не писали, тож збірка без source maps — файлів, що відображають результат назад на ваш код, — викинула ваші стектрейси. А TypeScript зникає до того, як щось виконується: він перевіряє форми, які ви оголосили, і не може перевірити відповідь із мережі, поки ви не валідуєте її самі.

А ще є те, чого ви не писали взагалі. npm install із трьома пакетами в package.json звично приносить сотні, бо кожен із ваших трьох має власні залежності, а ті — свої. Медіанна вебсторінка сьогодні надсилає на телефон 558 КБ JavaScript, і 44% цього не використовуються під час завантаження сторінки (Web Almanac, 2024). Кожен із цих пакетів супроводжує людина, яку ви ніколи не бачили і яка будь-якого дня може передати його комусь або прибрати.

Ви обираєте три пакети; кожен приносить власні залежності, а ті — свої, тож тут встановлюється 25 пакетів, а в справжньому проєкті — сотні. Ризикований вузол рідко той, який ви обрали: це маленький пакет на три рівні нижче з одним супровідником, до якого ведуть одразу кілька ваших залежностей, — саме така форма була в left-pad у 2016 році та в event-stream у 2018-му. Lockfile фіксує точну версію кожного вузла, а npm ci встановлює рівно це дерево або падає. Дерево ілюстративне.

Діаграма показує, чому це структурний ризик, а не питання вибору добрих пакетів. Пакет, що створює проблему, рідко той, який ви обрали. Це листок на три рівні нижче, крихітний, з одним супровідником, до якого ведуть одразу кілька ваших залежностей, — рівно така форма була в left-pad у 2016 році та в event-stream у 2018-му, обидва є серед кейсів нижче. Заходи, що працюють, стосуються всього цього дерева, а не трьох ваших прямих виборів:

  • у репозиторії — package-lock.json чи його відповідник, — що фіксує точну версію кожного вузла дерева, а не лише трьох, які ви назвали.
  • npm ci замість npm install у пайплайні, щоб збірка встановлювала рівно те, що каже lockfile, або падала.
  • Оновлення, що приходять як переглянуті , із затримкою в кілька днів після релізу, — захоплені версії часто знаходять і знімають за години чи дні, і затримка тримає вас поза цим вікном.
  • Залежність додається лише після питання, скільки коштувало б написати це самому; функцію з одинадцяти рядків дешевше мати свою, ніж залежати від чужої.
  • Сторонні скрипти, які віддаються з вашого власного домену або прибиті хешем Subresource Integrity, а не підвантажуються наживо з домену, яким керує хтось інший.

П’ять кейсів і те, чого в кожному бракувало

Кожен кейс нижче задокументовано публічно — компанією, реєстром пакетів або дослідниками, що ним займалися. У жодному не бракувало чогось хитромудрого. Бракувало рядка замість числа, зафіксованої версії, рев’ю або скрипта, що віддається з власного сервера.

КейсЩо сталосяЧого бракувало
ID у Twitter, 2010Нові ID твітів от-от мали перевищити 53 біти, яких JavaScript точно не втримуєID, передані рядками, — додані як id_str (рядок 7)
left-pad, 2016Пакет з одинадцяти рядків зняли з публікації, і збірки Babel та Atom падали близько двох з половиною годинКопія того, від чого ви залежите, і реєстр, який не може видалити її з-під вас
SmooshGate, 2018Новий стандартний метод зламав сайти на старій бібліотеці, тож стандарт перейменував свій методНічого з боку сайтів — і саме тому мова зберігає свої старі помилки
event-stream, 2018Новий супровідник додав залежність, яка крала в користувачів одного біткоїн-гаманцяПереглянуті оновлення дерева, а не лише власного коду
polyfill.io, 2024Домен скрипта, що стояв на 100 000+ сайтів, продали, і скрипт почав перенаправляти користувачів телефонівСкрипт, що віддається з вашого власного сервера

left-pad — це дерево залежностей у найменшому масштабі. 22 березня 2016 року після суперечки через назву з компанією Kik розробник зняв з npm усі свої 273 пакети. Один із них, left-pad, складався з одинадцяти рядків, що доповнювали рядок до потрібної довжини. Він стояв під line-numbers, який просив рівно версію 0.0.3, а line-numbers стояв під Babel та Atom, — тож збірки по всій екосистемі почали падати за лічені хвилини. Хтось перевидав left-pad за десять хвилин, але як версію 1.0.0, а вона не задовольняла «рівно 0.0.3». npm відновив оригінал о 16:55 за тихоокеанським часом, приблизно через дві з половиною години, а потім змінив правила, щоб пакет, від якого залежать інші, більше не можна було просто зняти.

Останні два кейси — та сама атака на двох різних рівнях. event-stream була бібліотекою потоків із приблизно 1,5 млн завантажень на тиждень, і від неї залежало близько 1 600 пакетів. У 2018 році її автор, який сам нею вже не користувався, прийняв пропозицію допомоги й дав права на публікацію незнайомцю. Незнайомець додав залежність flatmap-stream, чий вміст спав, доки не опинявся у збірці біткоїн-гаманця Copay, а там намагався вкрасти ключі гаманців із великими балансами. Це знайшли в листопаді, після мільйонів встановлень. polyfill.io був тегом script на понад 100 000 сайтів, що підвантажувався наживо з чужого домену. У лютому 2024 року домен і GitHub-акаунт проєкту продали, і до червня скрипт перенаправляв користувачів телефонів на сайти ставок. Його початковий автор уже встиг сказати всім прибрати його, бо сучасним браузерам він більше не потрібен.

Де це вироджується

JavaScript рідко ламається так, щоб зупинити сторінку. Він ламається тим, що працює повільніше, ніж мав би, губить помилку там, куди ніхто не дивиться, або постачає код незнайомця. Ось форми, яких це набуває.

  • Незалежні виклики, очікувані поспіль, перетворюють 400-мілісекундну сторінку на 900-мілісекундну без видимої причини.
  • async-колбек, переданий у forEach, повертається одразу й нічого не чекає.
  • Проміси, створені й ніколи не очікувані та не спіймані, у Node.js завершують процес.
  • || для значень за замовчуванням там, де мався на увазі ??, — і чинний 0 чи порожній рядок підміняється.
  • Гроші в числах із рухомою комою, де toFixed ховає похибку, доки не почнеться звірка підсумків.
  • Поверхневі копії, сприйняті як глибокі, — вкладена зміна досягає оригіналу.
  • Довга синхронна робота в головному потоці, яка морозить сторінку, хоч би як її написали.
  • Слухачі, які додають на кожному екрані й ніколи не прибирають, — вкладка росте, доки її не закриють.
  • Залежність, додана, щоб заощадити десять рядків, і живий тег script із домену, яким керує хтось інший.
  • TypeScript, сприйнятий як валідація, — дані з мережі вважають надійними, бо їхній тип оголошено.

Є й чесна межа, і вона не про мову. Мова скінченна. Усе з таблиці нагорі можна вивчити за тиждень, а більшість решти зловить лінтер. Чого вивчити неможливо — це дерево залежностей із кількох сотень пакетів, написаних людьми, яких ви ніколи не зустрінете, і жоден інструмент не прочитає його за вас: аудит розповідає про вразливості, про які вже хтось повідомив, а не про супровідника, який наступного місяця передасть пакет незнайомцю. Заходи з розділу про модулі зменшують ризик. Зменшити саму відкритість до нього можуть лише менше залежностей.

Друга половина межі — пристрій. Ніщо в мові не скаже, як ваш код відчувається на Android-телефоні середнього класу зі слабким з’єднанням, а саме там значна частка ваших користувачів. Сторінка, що відповідає за 80 мс на ноутбуці розробника, на телефоні може витрачати в кілька разів більше. Відкрийте панель Performance з увімкненим уповільненням , а ще краще — візьміть справжній телефон і пройдіть ту одну задачу, заради якої сторінка існує. Це єдина перевірка на цій сторінці, що вимірює те, що відчуває користувач, а не те, що каже код.

Коли це застосовувати

Застосовуйте, коли

  • Усе, що виконується в браузері: альтернатив немає, є лише мови, що компілюються в неї.
  • Додавайте TypeScript, щойно в проєкті більше одного учасника або він переживає пам’ять однієї людини.
  • Беріть Web Worker, коли робота справді впирається в CPU й не має блокувати інтерфейс.
  • Беріть Promise.all щоразу, коли очікувані виклики не залежать один від одного.

Уникайте, коли

  • Покладатися на нестрогу рівність чи «правдивість» там, де 0 або порожнє значення є чинними.
  • Числа з рухомою комою для грошей — рахуйте цілі дрібні одиниці.
  • Важкі обчислення в головному потоці, коли користувач має продовжувати взаємодію.
  • Вважати, що TypeScript валідує дані під час виконання, — ні; розбирайте те, що перетинає межу.

Джерела й метод

На чому стоїть ця стаття

  • 12 джерел
  • 3 задокументовані збої
  • Перевірено 13 жовтня 2026 р.

Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.