Галоўная / Артыкулы / Двадцять пяць сцэнарыёў спрашанняў на JavaScript, створаных на адной з рэальных проблем у продакшыне.

Двадцять пяць сцэнарыёў спрашанняў на JavaScript, створаных на адной з рэальных проблем у продакшыне.

Гонкі, трэш, ідемпотентныя платежы, витекі, гідратацыя, кэш WeakMap, нестабільныя тесты і спільныя шары запитоў — з адказамі, якія насправды хочуць спрашваючыя.

4513 слоў

Адаптаванае з AI

Двадцать пяць запытанняў на JavaScript у формате рэальных задач

У карточках для адзінакоўства ў спытаннях яшчэ часта запытаюць, што вяртае typeof null, як працюе механізм хоўстінгу, чым можна заменіць bind. Такія запытання лёгкія да оцэнкі — і лёгкія да падрабання пасля кількох дзён запамятоввання. А потым той самы кандыдат працюе з пошуковым полем, яке паказвае рэзультаты для запытання, якое ўжо было адміністрацыяю старанна вычыставана.

Команды, якія маюць рэальны трафік, зменілі формат. Узамест таго, каб запытаць пра лекцыю пра цыкл змаганняў, яны паказваюць застойлены экран аналітыкі пасля змены дзеяння часовага перыяду, даюць фрагмент коду і запытаюць, што йдзе не так. Тое ж сама фундаментальная інформацыя; разны сігнал. Адна метода пераканальваецца ў запамятовванне. Іншая пераканальваецца, чы можа хтось аналізаваць незнайомую систему пад старанным наглядам.

Эта прыжкавасць найбольша ў трох зонах: асінхронна праця ў рэальных сетях (атрыбуты, якія прыходзяць не па порядку, обявы, якія выкааналізаваюцца, але вже не маюць значэння), памяць (на ноутбуке, які застаўся відкрытым на шасць хвілін, нічога не зламваецца) і абыякавасць (продавец разам з кодам HTML з памылкай вяртае статус HTTP 200, і response.json() выклікае крах о 2 гадзіны ночы).

Далей праказана 25 сцэнарыяў, запоўненых прыкладамі багоў, якія фактычна з’являюцца ў працоўным сераверы: медленные панелі керування, дубліруючыя запиты, зростаючая памяць праз змены маршрутаў, застарэлыя рэзультаты пошуку, подвойныя платежы, несправныя прыпускі ў аутэнтыкацыі, табліцы з 20 тысячямя рядкамі і API, якія працуюць 96% часу. Кожны пункт включае ситуацыю, адпаведны адказ, які шукаюць у спэкеры, короткі фрагмент коду, распашчасты неправільны падход, можлівы наступны крок і тое, што вымераецца.

Прыёмкі спачатку даюцца на англійскай мове, пры чым прымечанні на TypeScript даюцца толькі там, дзе типы вплываюць на рэсультат. Рагі: «Пачаткавец» — для падготовкі да среднага рэвэлю, «Середні» — для большасці скрынь высокага рэвэлю, «Развіты» — для ведчыцтва дыялогаў з спеціялістамі.

Пачаткавец — асновы працы ў продакшэне

Гэта разлічае тых, хто вже выпускаў продукт, ад тых, хто толькі завершыў навучальныя матэрыялы. Нічога складнага; усё гэта можа выйсці з працай у рэальных дапрацоўках.

Debounce проты Throttle для пошуку і прасування

Сцэнарый. Поле пошуку адправляе запит пасля кожнага натыкання на клавішу. Праблема — зменшыць колькасць запытанняў, не робячы набіранне тексту мертвым. Обробнікі прасування ў іншых частках прагледзеця патрэбуюць сталяга, але обмежанага апдэйта.

Запытанне. Калі debounce є правым інструментам у працы проты Throttle, і як правільна ўжываць яго ў React?

Адказ. Функцыя throttling гарантуюе выконанне запытку па фіксаванай частоте — гэта падходяга для прасування та змены размеру. Функцыя debounce чакае, пакуль не прынікне пауза ў набіранні тэксту, што адпавядае намеру корыстніка выконваць пошук. Пачніце з перыяду ад чвяртай секунды да трохісот мілісэкунд і налаштавайце яго на адзінку з показнікаў. Спалучайце debounce з мінімальным дзейснім дужынай запытку та з можлівасцю анулювання запытку; сама функцыя debounce не выправляе адпаведнасці запыткаў да парадку ў яком вы яны надаюцца.

function debounce(fn, wait = 250) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

const search = debounce((q) => {
  if (q.length < 2) return;
  fetchResults(q);
});

Неправільны падход. Стварэнне новага обгорткаўскога коду з викорыстаннем debounce на кожны раз пад час адрасавання элемента. Таймеры перазначаюцца па новыя контексты, таму функцыя debounce насправдзе ніколи не працюе. Стабілізуйце ситуацыю за дапамою useMemo/useRef і ачышцайце ўсё пад час зняцья элемента.

Даадзейненне. Корыстнік быстра набирае тэкст, а потым ачышчае поле. Функцыя debounce все равно выконвае запытку з апошнім не-пустым значэнням. Як взаімаюцца можлівасць анулювання запытку пад час зняцья элемента та пад час ачышчэння поля?

Зьвяржаныя данні. Выбор інструмента залежыць ад цілей UX, а таксама ад зручнасці паводле закрыццяў, якія застаюцца стабільнымі праз рэндары.

Двадцать тысяч абэрантав клікаў

Сцэнарый. Сітка дадзеных прыкладзея onClick да кожнай дзеяння ў рядку. Пры 20 000 рядках стораніца заставаецца некія секунды, пакуль не стане інтэрактываю, а выкарыстоўваная памяць зростае праз перегляд стораніц.

Запытанне. Як следуе пераструктураваць обробку здарэнняў?

Адказ. Ўжыце дэлегаванне здарэнняў. Один абэрант на контейнеры чытае цэль, калі здарэння прайходзяць далей — адна функцыя ў памяці, без перывязвання, калі змінююцца рядкі, і гэта працуе для рядкаў, якія дадаюцца пазней. Ідентыфікуйце рядок і дзеянне за дапамою атрыбутаў дадзеных і closest, таму што клікі часта падаюць на іконку ўнутры кнопкі, а не на саму кнопку.

grid.addEventListener('click', (event) => {
  const button = event.target.closest('[data-action]');
  if (!button || !grid.contains(button)) return;

  const { action } = button.dataset;
  const rowId = button.closest('tr')?.dataset.rowId;
  handleAction(action, rowId);
});

Няўерны падход. Яшчэ працуецца з тыяўчамі слухачаў роўнаў і спадзяецца, што цикл чысткі ўсё гэта адключыць. Косты налаштавання застаюцца, а несувяжаныя канфігурацыі функцый бесшумна не можаць быць ад’ёжаныя.

Даадзеныя. У React 17+ дзе бібліятэка рэгіструе свой корневы слухач, і як дэлегаваныя натыўныя працоўнікі должны сусіставаць з сынтэтычным распространенням адбуванняяў?

Ацэнка. Чы розглядваемы кандыдат спрыягае колькасці слухачаў як да справжньяго бюджету на выданнее, а не проста як да стылю.

Прабамба, якая вырашылася, — гэта не тое ж самае, што прабамба, якая ўсё ўжо мае значэнне.

Середнія навыкі — асінхроннае програмаванне, памяць і выданнее

Сюды ж прыводзяцца рашэння ў большасці інтэрв’ю для кандыдаў высокага рангу. Запитанні нагадваюць сесіі дыбаггінгу, таму што гэта і є ўласнае заведанне.

Панелі керування, якія замарзаюць на два секунды

Сцэнарыю. Змена дыяпазона дат замарожвае вікно. Клікі не дзейнаюць, анімазіі зупыняюцца, спінер ніколі не круціцца. Сама сетавая запытка займае 180 мс.

Пытанне. Чаму спінер не анімаваецца, і куды зник час?

Адказ. Галоўны поток мультіплексуе скрыпты, лейаут, адрасаванне і вхідныя даны. Якщо трывае запытка досыць дзяўна, нават спінер не можа быць адрасаваны, таму што фрагмент экрана, які мусіў бы яго паказаць, ніколи не выконваецца. 180 мс — цэла сетавая час; замарожванне — сінхронная праця пасля таго: парсаванне вельмі большога JSON-пакета, а потым выконанне оперэйцый map/filter над дзесяткамі тысяч рэядоў, пры чым кожны з іх выдзеляе новы масіў.

Перакладзіце важкія працэсы ператварэння ў Web Worker, або раздзеліце працу на часткі і робіце перапыткі между ямі, а таксама, калі можна, запытайце бэкенд пра аграгаваныя даны.

// Yield to the event loop between chunks so input and paint can run
async function processInChunks(items, fn, chunkSize = 500) {
  const out = [];
  for (let i = 0; i < items.length; i += chunkSize) {
    for (const item of items.slice(i, i + chunkSize)) out.push(fn(item));
    await new Promise((r) => setTimeout(r, 0));
  }
  return out;
}

Няўерны падход. Адналікваванне async/await у цікле, які выкарыстоўвае многа РЦ, і называнне яго безблокіруючым. Не ствараецца жадных дапаможных потакоў; сінхронная робота як і раней заморажвае вікно.

Дааднэйшыя пытанні. Паспяшыце поясненне адрозніцы мікрзадач і макразадач у гэтым заморажванні, а таксама прычыны, чаму викорыстанне await Promise.resolve() все рава залеўвае дзялянкі з дужа дугім сінхронным выкарыстоўванням ресурсаў.

Замеры. Розумеўце паралельную роботу JS як планаванне задач і ведаце, калі неабходны дапаможнія потакі.

З AI

Рэзультаты пошуку для запита, які ўжо выдалены корыстнікам

Сцэнарый. Пры введенні “sam”, а пасля — уточнэнні да “samantha” іноды паказваюцься рэзультаты для “sam” пасля таго, як вже з’явіліся рэзультаты для “samantha”. Це важкая справа да відтворэння на шырокаспектным з’ѐеднанні.

Пытанне. Што самэ працуе, і які ўсунуць гэтую проблему правільны спосаб?

Адказ. Конкурэнція межа адпаведзеннямі, якія надаюцца не па порядку. Працуюць два запиты; медленнейшы належыць да старэйшага запиту; той, які будзе адпаведзены последнім, выграе парадокс змены стану. Лепш выкарыстоўваць оба спосабы зменшэння проблемы: анулюйце паканальны запит за дапамою AbortController, а таксама захавайце можлівасць зберагання стану так, каб толькі адпаведзенне на текущы запит магле быць зафіксаванае.

const controllerRef = useRef(null);

async function search(query) {
  controllerRef.current?.abort();
  const controller = new AbortController();
  controllerRef.current = controller;

  try {
    const res = await fetch(`/api/customers?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    setResults(await res.json());
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
  }
}

Няправільны падход. Растрынуванне періяду затрымкі да цэлага секунды і прызнанне перамогі. Конкурэнція стае рэдкай, корыстнасць пры использованні продукту падупае, а медленныя сеті яшчэ пераранжуюць адпаведзення.

Дааднейшыя пытанні. Калі ідэнтыфікатор запиту, який адрастае монотанічна, можа перамогць у выкарыстоўванні для адклэяння застарелых дадзеных запыту?

Мераванне. Правильнае называнне канкурэнцій і выкарыстоўвання інформацыі пра анулювання запытоў без падачы яе ў панелі адзначэнняяў пра бягу.

Той самы запит, пяць разоў

Сцэнарыю. Пяць компанентаў пад час запуску атрымліваюць даны з /api/current-user. Карточка сеті паказвае пяць ідэнтычных запыткаў; часам адпаведны адпаведзень стае застарэлым пасля адчынення профілю.

Запытанне. Як усунуць дуплікацыю запыткаў без перапісваў цэлага шару дадзеных?

Адпаведзень. Зберагаць у кэшы promise, а не самы рэзультат. Ключ мапы ствараецца на адпаведнасць ідэнтыфікатору запытка, а спяльны promise вяртаецца кожнаму, хто яго запускае; ключ адчыняецца, калі запытак завершыўся, ў такі спосаб працэсы з нявыпаленными запыткамі можна прабаваць знову, а свежыя даны — атрымліваць пазней. Бібліятэкі, такія як TanStack Query і SWR, дапамагаюць рэалізаваць правіла недзейснавання і выяўлення застарэлых дадзеных на адной з гэтых падстав.

const inFlight = new Map();

export function dedupedFetch(key, fetcher) {
  if (inFlight.has(key)) return inFlight.get(key);

  const promise = fetcher().finally(() => inFlight.delete(key));
  inFlight.set(key, promise);
  return promise;
}

// All five callers receive the same promise
const user = await dedupedFetch('/api/me', () => fetch('/api/me').then((r) => r.json()));

Няправільны падход. Вечна зберагаць завершаны пакет дадзеных у змэнны на рэвэлі модуля. Дуплікаты запыткаў зникаюць, але інтерфейс паказвае даны вчорашняга корыстніка, пакуль хтось не атрымае свежыя даны за дапамогою функціі hard-refresh.

Даўжэйшыя наследкі. Якщо два апеллянты прыўяжу разныя сігналы аббаркання да спакойнага обявлення, яке выкарыстоўваецца пад час выканання, якая політіка аббаркання будзе дапамагаць застацца правдазнаўнымі для обох?

Меркаванне. Актыўнае спакойнае выкарыстоўвання обявлення пад час выканання і планаванне ўнічогаўлення заздалегідь.

Аднаму ненадзеяму прадавцу не хватает кантролю над сторанай

Сцэнарый. Профіль, вырахунакі і віджет з рэкамендацыямі трохі чужых аплікацый завантажаюцца разам. Доступнасць прадавцу становіць ~96%. Калі ён перстане працаваць, вся сторана паказвае адныя толькі памылкі, а вырахунакі стаюць недоступнымі.

Пытанне. Як следуе пераструктураваць процес запрашэння дадзеных, і якія тут разліки между Promise.all і Promise.allSettled?

Адказ. Promise.all абрабатвае калі хоць яны з вхідных дадзеных абрабатвае няправільна, і тады ўсе успешны элементы працягваюць работу. Promise.allSettled завжды абрабатвае кожны элемент і вяртае статус кожнага з ягох — паказвае тыя, якія успелі, а рэшту працягвае з меньшымі можлівасцямі. Бюджетаванне і прафіль павінны застацца на критычнай дорозе; рэкамендаціі павінны маты короткі термін выпалення, ўбліжчаючы такім чынам час на ўсуненне проблем, калі постаўщык можа зупініць работу, нават як пазней будзе вяртаць успех.

const [profile, billing, recs] = await Promise.allSettled([
  getProfile(),
  getBilling(),
  withTimeout(getRecommendations(), 2000),
]);

if (profile.status === 'rejected' || billing.status === 'rejected') {
  return renderError();
}
render({
  profile: profile.value,
  billing: billing.value,
  recs: recs.status === 'fulfilled' ? recs.value : [],
});

Няправільны падход. Пераклад чыненняў абрабаткі ў null, каб Promise.all продавяла работу. У такім разе тэлеметрыя не можа зафіксаваць прычыну абрабаткі няправільна, і всі чыненняя выглядаюць аднойчыны.

Далейшыя крокі. Выберыце варыянт ад Promise.any да Promise.race і чытайце, што будзе з тымі прэсаментамі, якія не будуць успешныя.

Меркаванне. Вмеласць у суворэй камбінацыі прэсаментоў плюс інстынкты ўжо да наследків такіх дзеянняў.

Адзякаванне, якое было выкарыстоўвана два разы

Сцэнарый. Часы адбору платежа выканчваюцца на шлюзе. Фронтэнд прабуе зноў. Кліенту выраховваецца адзякаванне два разы.

Пытанне. Какая стратэгія прабоўкаў є безпечной для канцэнтра платежаў?

Адказ. Прабоўкі є безпечнымі толькі для ідэмпатных операцый. Запрос POST, які стварае адзякаванне, за замовчаннем не ўсё ідэмпатны — зробіце яго такім за дапамогою ключа ідэмпатнасці, створанага кліентам, на які сервер будзе адмахватыцца. Прабоўкі трэба выкарыстоўваць толькі у разе проблем з перадачай даных і адпаведных адпаведзенняў 5xx/429 — ніколі для 4xx. Раздзеляйце прабоўкі за дапамогою экспансійнага адступлення плюс джиттера, каб сервер, які восстанавляецца, не быў перанасытаны запросамі. Следавайце заголовкам Retry-After і задаўце жорсткі ліміт колькасці прабоўкаў.

async function postWithRetry(url, body, key, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status < 500 && res.status !== 429) throw new HttpError(res);

    const backoff = 2 ** i * 300 + Math.random() * 300; // jitter
    await new Promise((r) => setTimeout(r, backoff));
  }
  throw new Error('Payment could not be confirmed');
}

Няправільны падход. Абслюцыдна прабоўкаваць усё тры разы за фіксаваным часам. Таймауты могу выраховаць два адзякаванні, цыклы 4xx ніколи не будуць успешныя, а перывы прыводзяць да ўскладненняў.

Дальнейшыя дзеянні. Браузер выканаў тайма-аут, але платеж быў завершаны на серверах — раскажіце пра спосабы вярнення стану, видныя для пользователя.

Замеры. Дизайн, які застаецца функцыональным нават у разы неяснасцей на стороне кліента.

API, якое застаецца у стане завісу, а не выдае памылку

Сцэнарый. Прадаўца перестае адпавядаць, не закрываючы з’ѐеднання. Функцыя fetch ніколі не завершаецца; обробнікі задач накапліваюцься; індыкаторы загрузкі працуюць хвілінамі.

Пытанне. Як можна контролюваць гэта, і што ўключаецца па захадзе тайма-ауту?

Адказ. Навігаторы не задаюць значэння таймауту для fetch за замовчаннем — яго трэба задаць самостайна. AbortSignal.timeout — это савэцкі падход; AbortSignal.any спалюе таймаут з можлівасцю анулювання з боку пользователя. Таксама трэба выкорыстоўваць механізм «circuit breaker»: пасля аднойчыных няудач трэба перастаць выкананне запыткаў на час ахалення, ў той час як негайна будзе выдавана альтернатыўная інфармацыя, чым захіщаецца як сам прыстрой, так і залежнасці, якія прабуюць восстанаць.

const signal = AbortSignal.any([
  AbortSignal.timeout(3000),
  userController.signal,
]);

try {
  const res = await fetch(url, { signal });
  breaker.recordSuccess();
  return res.json();
} catch (err) {
  breaker.recordFailure();
  if (err.name === 'TimeoutError') return cachedFallback();
  throw err;
}

Неправільны падход. Адначасовае выкананне fetch і таймера, з якім выдумваецца, што працэс, які заканчыўся раней, быў зупінены. HTTP-запыт продовжае працаваць, утримвае сокеты і можа ўсё ж зменіць стан, калі нарэшце завершыцца.

Дальнейшыя заходы. Трэба задаваць таймауты як у самом навігаторе, так і ў API-шлюзе, а таксама у запытках Node да таго ж постачальніка, каб не заставалася незавершаная робота.

Метрыка. За замовчаннем трэба не даваць доверыі залежнасцям і рэальна анулюваць запыткі, ў той час як трэба ужосткаць контроль над незавершанымі задачамі.

Памяць зростае праз кожную зміну маршруту

Сцэнарый. Адпаратны інструмент, які заставалі ачытым увесь дзень, заполняеся да 1,4 ГБ. Знімкі кашы паказваюць, як відокремленыя вузлы DOM зростаюць у колькасці з кожным переходам між запитамі.

Запытанне. Якія звычайныя прычыны, і як іх парадактуваць?

Адказ. Відокремленыя вузлы застаюцца жывымі, таму што якась частка ўсё ўсёлякі спосабом да яных апыляецца: слухачы window/document так і не былі адключаны, setInterval продовжае працаваць, IntersectionObserver/ResizeObserver так і не былі ад’ўязаны, слухачы глобальнага хранення, а таксама клоузуры ў довгавучых кэшах, якія фіксуюць структуру DOM. Парадактуваць можна, зробіўшы знімок кашы, перейшоўшы на іншы маршрут, выканавшы прымусовую збірку сметлівых дадзенняў, зробіўшы знімок знову, а пасля — працаваўшы зі спісамі власнікаў дадзенняў, пакуль не будзе ясна прычына ўтримання вузла.

useEffect(() => {
  const onResize = () => recalcLayout();
  const observer = new ResizeObserver(onResize);
  const id = setInterval(pollTicket, 5000);

  window.addEventListener('resize', onResize);
  observer.observe(panelRef.current);

  return () => {
    window.removeEventListener('resize', onResize);
    observer.disconnect();
    clearInterval(id);
  };
}, []);

Няўерны падход. Скасаванне локальных зменных пад час чысткі і спадзяванне на змену колькасці памяці. Доступнасць даных продаваецца чераз жывыя слухачы і таймеры, якіе выкарыстоўваюць тыя ж даны.

Далейшасць. Шаблон вытэкання памяці можна назваць WeakMap; ён чыста рашыяе проблему, а ситуацыю, калі слабкія ссылкі є непадходячым способам, таксама можна адзначыць.

Апранаванне. Практычная налагодка хіпа і розуменне прынцыпаў доступнасці даных.

Лямпа, якая завжды фіксуе ноль

Сцэнарый. Віджет перакантролюе стан кожныя пяць секунд і дадае апавешчэння. Ён завжды паказвае аднае апавешчэнне; лог-запіс у межах цаговага інтэвалу вечна прадрукавае пачатковы стан.

Пытанне. Чаму інтэвал бачыць застарелы стан, і як гэта паспрацаваць?

Адказ. Эфект выкананы адзін раз з [], таму калбэк функцыя працавала з станом першаго адражэння. Апдейты ствараюць новыя значэнні; старая калбэк функцыя яшчэ супрацоўвае з старым значэннем. Лепш выкарыстоўваць функцыональныя апдейтары, каб React задаваў найсвежэйшае значэнне з лісты апдэйтаў. Калі калбэк функцыя патрабуе дадзеныя, якіх не можа даць апдейтар, іх трэба копіюваць у ref на кожным адражэнні.

// Broken: `alerts` is frozen at the first render
useEffect(() => {
  const id = setInterval(() => setAlerts([...alerts, poll()]), 5000);
  return () => clearInterval(id);
}, []);

// Fixed: functional update, no stale capture
useEffect(() => {
  const id = setInterval(() => setAlerts((prev) => [...prev, poll()]), 5000);
  return () => clearInterval(id);
}, []);

Няправильны падход. Указванне alerts як залежнасці эфекта. Старыя калбэк функцыі зникаюць, але інтэрвал пачынае працаваць занова ў кожны раз, калі дадаецца новы элемент, таму частота адражэння падае да пяці секундаў.

Дааднейшыя пытанні. Як можа спецыяльны хук падтрымваць частоту адражэння пяці секундаў, пры чым завжды чытаць свежы стан?

Меркаванне. Калбэк функцыі, якія «паўтараюць» свою працу ў часе — класычны проблема середньага рангу ў React.

Двадцать тысяч рэядоў у DOM

Сцэнарыю. Таблыца інвентару адраджвае кожны запис API. Апляціяванне фарбы трывае шасць секунд; фільтрацыя споўзае; карточка выкорыстоўвае сотні мегабайтаў.

Пытанне. Як зробіць таблыцу прыдатной для вжывання і што меркаваць першым?

Адказ. Спачатку аналізуйце структуру: скрыпты, стыль, макет і процес апляціявання фарбы. Для таблыцаў такога розмеру зазвычай главную ролю выіграўа колькасць вузлаў DOM. Віртуалізуйце спіс так, каб у DOM існаваў толькі вікна перагляду плюс невялікий буфер для дадатковага сканавання. Спалучыце гэта з стабільнымі ідэнтыфікаторамі рядоў, акуратным збераганнем дадзеных у памяці, а таксама фільтрацыёю/сортаваннем на староне сервера, калі наборы дадзеных на староне кліента стануць занадта вялікімі.

// Row identity matters as much as row count
{visibleRows.map((row) => (
  <Row key={row.id} data={row} />   // stable id, not the array index
))}

Няправильны падход. Аплыванне рядоў прымасцем React.memo і завершэнне на гэтым. Новыя атрыбуты ў формате inline скасоўваюць эфект мема, а макет і процес апляціявання фарбы все равно страждаюць пад тысячамі вузлаў.

Даўжэйшыя наследкі. Чаму выключаецца фокус і контрольваныя вводы, калі клавішы роўнаў ёсць індэксамі масіву, а середняй роўн выкарыстоўваецца не?

Замеры. Звычкі пачынаку з мерэння і рэалістычныя межы мемаізацыі.

З AI

Проблемы з распаўненням лейауту ад ціклавых задач фільтрацыі/змены размеру

Сцэнарый. Пасля завантажэння дадзеных панель керування зменяе размер контэйнераў дыяграм. У прафілях паказваюцца дужыя фіолетавыя блокі стылю/лейауту, якія павтараюцца сотні разоў у адной кадре.

Пытанне. Якая прычына гэтага явішча, і як яго выправіць?

Адказ. Проблемы з распаковкай лейауту. Адкрыцья геаметрычных API, такіх як зсувы вышыні, прасторы або пазіцыі прасування, вымагае сінхроннай перзапісі стылю і лейауту, каб двойчык мог уздрэць точны адказ. Адменавляванне цых чытанняў з запісямі стылю ўнутрь циклу спрычынае такую перзапісь у кожной ітерацыі. Спачатку зберагайте памеры, потым зменяйце стылі, а запісы выканаўце за дапамогою requestAnimationFrame. Для логіки паказу/закрыцьця вядомаў скорыстаюцеся IntersectionObserver, каб відразлівасць была асінхронная без прымусовай перзапісі лейауту.

// Broken: read, write, read, write
panels.forEach((p) => { p.style.height = p.offsetHeight * 1.2 + 'px'; });

// Fixed: batch reads, then batch writes
const heights = panels.map((p) => p.offsetHeight);
requestAnimationFrame(() => {
  panels.forEach((p, i) => { p.style.height = heights[i] * 1.2 + 'px'; });
});

Няправільны падход. Выканаўце кожной запісы ў сваёй сабе анімацыйным кадром, пакуль чытання яшчэ адменавляюцца. Косты распадаюцца між кадрамі, і проблемы з адработкай трываюць дольш.

Даадзеныя. Калі CSS-аспекты застаюцца ў композітары, і калі will-change спрычынае больш костаў, чым зарабляе?

Зьвяржана. Косты працы браузера паказуюцца як последовальна сэрія дзеянняў, а не як чыганак з таінствамі.

Чаканне на сінхронны вычыткі не звалівае цыкл адбывання запускаемых задач.

Развітанне — безпека, Node, тэставанне і дизайн

У робіце спецыялістаў меньш важліва адна ідеальная рашытка, чым компрэсіі, можлівыя наследкі і адпаведная адпаведальнасць.

Поле CMS, якое выканало скрыпт

Сцэнарый. Кампанія з маркетынгу храніць багаты тэкст у CMS без веб-інтерфейса. Праця з безпекай паказвае, што на кожнай сторонцы товара выкананы код <img onerror=...>.

Пытанне. Як гэта сталася і як выправіць гэта ў всій системе?

Адказ. Значэнне падае ў innerHTML або dangerouslySetInnerHTML у React без санітазацыі. За замовчаннем выконваецца экрапсія (у React — {value}, у DOM — textContent). Калі патрэбны HTML-тэгі, неабходна санітазацыя за дапамою бібліятэкі з списаком дазволеных элементаў, такой як DOMPurify — і на серверы, таму што кліент не ўважаецца надзейным рубункам. Неабходна настаўіць Content Security Policy, каб пракрастаны пакет дадзеных не могаў выканаць вбудованный скрыпт.

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(cmsHtml, {
  ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'li'],
  ALLOWED_ATTR: ['href', 'title'],
});
<div dangerouslySetInnerHTML={{ __html: clean }} />

Няправільны падход. Адчысцэнне тэга <script> за дапамою регулярных выразаў. Атрыбуты обработчаў запуску, URL-адрэсы з javascript:, вектары SVG і вялікія роўні засунутага коду праходзяць без перакантролю.

Далейшыя заходы. Аналітыка вбудоввае вбудованный скрыпт, а CSP яго блакуе — трэба вярнуць тэг, не актываўваючы unsafe-inline.

Зьвяржана. Аблягаты проты XSS з санітазацыяй у момент адрасавання.

Токен у localStorage

Сцэнарый. Пасля атакі XSS зяўляюцца прыемкі ў чыстаце безпекі па сэсыйным токенам у localStorage, якія дадае інтэрцептар Axios. Яны хочуць план.

Запытанне. Калі выбіраць между localStorage і кукамі для токенав аутэнтыкацыі, і якія є рекамендацыі?

Адказ. Скрыпты на выхадзе можаць чытаць localStorage, таму адна жаховая атака XSS можа вывезці весь сэсію. HttpOnly Secure кукі з SameSite=Lax застаюцца невиднымі для JavaScript, пры тым браузеры даджаюць іх аўтаматычна, таму для змены запытоў все рава патрэбны захаванні ад CSRF. Частая схема: токен доступу на короткі час у памяці, токен апавежэння ў HttpOnly куцы, токены CSRF або параметр SameSite для змэн, перзамена токенав праз апавежэння. Нічога не застаецца недастрыжаным пасля XSS — запобежчэнне XSS застаецца прыоритетным.

// Server side, Express
res.cookie('refresh_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/auth/refresh',
  maxAge: 1000 * 60 * 60 * 24 * 7,
});

Няправільны падход. Шифраванне токенав у localStorage з ключам, які таксама знаходзяцца ў JavaScript-коде стораны. Кожны, хто можа чытаць даныя зберагання, можа таксама прачытаць ключ — гэта проста фарсы.

Дапытанне. Якшто токены доступу застаюцца толькі ў памяці, як нова відкрытая карточка можа отрымаць сэсію?

Зьвяржана вялічынь. Выбор спосабу зберагачвання токенаў на адзе па модэлю апазоры, а не проста слоганы.

Канцэнтр пункту Node, які спамоцавае ўсе іншыя канцэнтры

Сцэнарый. Калі запускаецца працоўнік для обработкі PDF-звястакоў у Express, гэта вызывае тайма-ауты некалязаных перагледоў стану системы і рост показніка p99 у всім сервісе.

Пытанне. Чаму адзін канцэнтр пункт вплывае на ўсе іншыя, і як гэта паспрацаваць?

Адказ. Node выконвае JavaScript-код аплікацыі на аднай ніті. Робота, якая залежыць ад CPU, блакуе цыкл задач — ніякіх іншых запитоў, таймераў чы абявлень пра ввод-вывод. Перакладзіце роботу, якая сильна навантажвае CPU, на worker_threads чы на зовнішняя канцэйна, ўжо каб HTTP-працоўнік толькі дадаў задачу у канцэйну і адпаведзе. Адправляйце вялікія даныя у формате стріма, а не зберагайце іх у буферы. Вядомаюць асінхронныя API для крыптаграфіі замест Sync-варыянтов; ніколі не вжывайце readFileSync на шляху запита.

import { Worker } from 'node:worker_threads';

app.post('/reports', async (req, res) => {
  const job = await queue.add('generate-report', req.body); // returns immediately
  res.status(202).json({ jobId: job.id, status: 'queued' });
});

// Or for in process CPU work
const worker = new Worker('./report-worker.js', { workerData: params });

Няўерны падход. Чакаецца выканання задач, якія залежнаць ад CPU, і спадзяецацца, што цыкл запуску справ будзе працаваць. Метод расшырэння выкарыстоўвае больш адзінакоў, якія таксуюцца за выкарыстоўванне ресурсаў, а кожны з іх яшчэ застоіваецца пад час обробкі таго ж типу запита.

Далейшыя пытанні. Калькі сигналы з рэальнага сервісу паказваюць, што цыкл запуску справ у Node заблокаваны, прычаму кліянты ўжо не жалуюцца?

Замеры. Раздзелэнне задач, якія залежнаць ад IO, ад тых, якія залежнаць ад CPU, паўтарныя сигналы з рэальнага сервісу для парадоражу.

Няўерны контэнт пад час заваношчання (хайдратацыя)

Сцэнарый. Аплікацыя Next.js атрыбутуе серверу задачу на прадарожчанне тэкста “Увайсці”, а пасля заваношчання пераходзіць да імені пользователя. Паведамленні пра несувярэннасць хайдратацыі; інодзе з’яўляецца паведамленне Тэкстовы контэнт не сувярэнны з HTML, прадарожчаным на сервере.

Пытанне. Чыя прычыны несувярэннасцей хайдратацыі, і як выправіць гэту проблему?

Адказ. У сервера няма элементаў, які існуюць толькі ў прыгледачы: кукіяў, якія чытае кліент, localStorage, window.matchMedia, Date.now(). Процес гідратацыі выклекае, каб першая відраслівая структура кліента падабалася структуре сервера. Пры незастаноўкі → React адмовляецца ад маркапаў сервера для той відраслі і паведамляе пра гэта. Чырадзіце сесію пад час рэндараў сервера, чытаячы кукіяў на сервере і перадаючы гэтыя значэнні ў структуру. Для значэнняў, якія практычна існуюць толькі ў прыгледачы, адобразіце стабільны заместак і апдэйтуйце яго пасля маунта.

// Server component reads the session, so no mismatch
export default async function Layout({ children }) {
  const session = await getSession(cookies());
  return <AuthProvider value={session}>{children}</AuthProvider>;
}

Няправільны падход. Затыканне паведамленняў пра гідратацыю на рэндеру-обгортку або дынамічнае змена заголовка, каб ухіліцца ад SSR. Неазастаноўкі застаюцца; прынады SSR зникаюць.

Даадзеўнае пытанне. Как можна адобразіць стаўкі часу на сервере без таго, каб процес гідратацыі суперсвайчваў з часам кліента?

Зьвяржана вялічынь. Правильна наладка гідратацыі замест тым, каб выключаць апавяшэнні.

Кэш, які ніколі не апускае

Сцэнарый. Бібліятэка для стварэння дыяграм зберагае кэш канвасоў, прыкрепленых да элементаў DOM. Памяць расте пасля таго, як дыяграмы пакідаюць сторанку. Знімкі памяці паказваюць буферы, якія застаюцца ў Map.

Пытанне. Чаму элементы не скарыгваняюцца, і як WeakMap змінюе рэзультат?

Адказ. Сістэма адзбору сметлі можа даўся да элемента з кораноў дрэва. Звычныя элементы Map прыкрепляюць як ключ, так і значэнне, таму элемент DOM, які стаў ключом, заставляе буфер канваса застацца доступным. WeakMap прыкрепляе ключы слаба — калі нічога іншаго не абырае гэты элемент, элемент можа зьнікнуць разам з яго. WeakMap не ўможлівае перэлачванне і не мае размеру; гэты компроміс робіць яго безпечным.

// Leaks: the Map keeps removed nodes alive
const cache = new Map();

// Collectable: entry dies with the element
const cache = new WeakMap();
cache.set(chartEl, renderedCanvas);

Няўерны падход. Аперацыі на кшталт Cron, якія вычыраюць элементы кэша, чыёў узлы пакінулі документ. Усе гаразд, пакуль пра такую аперацыю не забудуць; WeakMap пазбегае такой проблемы.

Далейшасць. Калі є падходжым FinalizationRegistry, і чаму правільнае працаванне не можа залежаць ад часу його запуску?

Аналіз. Алгорытм адчысцэння памяці як перакананне ў досяжнасці элементаў, без адгуку на тое, чы можна контролюваць час ўзяткі памяці.

Тэст, яны не праходзіць кожных двадцать запускаў

Сцэнарый. Тэст компонента для пошуку праходзіць локальна, але не праходзіць у середавышчы CI прыбліжна у 5% случаў пад час пошуку тексту “Samantha”. Камусь вже дадаў waitFor(3000), і CI прабуе зноў.

Пытанне. Як зробіць тэст надзеяным, і што можа сказаць його ненадзеянасць?

Адказ. Тэсты типу “flake” зазвычай перакантролююць час адпаведзення, а не паведамленні. Для кантролю прычын затрымкаў выкорыстоўваюць фальшывыя таймеры, а HTTP-запыткі на выхадзе заменяюць на ўсё болей складныя рэшэнні, напрыклад MSW, каб падтрымаць стабільнасць данных. Таксама выкорыстоўваюцься запыткі, якія чакаюць на апдэйты DOM, у замест на фіксаваны час спячкі. Якщо для працэўкі тэсту неабходна дзейсная спячка, гэта часта значыць наявнасць справжней конкурэнцыі ў компаненте — тады тэст правильна фіксуе проблему.

test('shows results for the final query', async () => {
  vi.useFakeTimers();
  render(<Search />);

await userEvent.type(screen.getByRole('searchbox'), 'samantha');
  await vi.advanceTimersByTimeAsync(300); // debounce window
  expect(await screen.findByText('Samantha')).toBeInTheDocument();
});

Няправільны падход. Павтарэння тэстаў у CI і ўсё большыя таймауты. Сігналы стаюць шумам, і тэст, які павтарыўся трохце разоў, можа замаскаваць справжню проблему, якая пазней выйдзе на продакшэн.

Дааднасці. Як тэст можа змусіць старэйшы адпаведзенне на пошук прыйсці пасля новэйшага?

Меркаванне. Разгляд “flake” як сігналу і ператворэнне асінхронных тэстаў у детерміністычныя.

Шасцьдзесят месцаў, якія выклікаюць fetch

Сцэнарыю. У чатыргодовага кодаваяку ў шасцудзесят кампанентаў выклікаецца fetch. Карыстоўванне механізмаў обробкі абэктаў ёсць няеднакавым; процэс апавежання пра автентыкацыю копіюўцца адзинадцать разоў; ніхто не знае паказнікаў дзейневых таймаутаў.

Запытанне. Як спроекаваць спяльны шар доступу да дадзэнняў і як перайсці на новую структуру без пазырання роботы системы?

Адказ. Аб’еднайце спяльныя функцыі ў аднам кліентскім коде: базовая URL і заголовкі, адзинственны процэс апавежання пра автентыкацыю, каб множлівасць абэктаў 401 прыводзіла толькі да аднаго процэса апавежання, насталенне таймаутаў, практыка перапрыбуткі толькі у разы ідэмпотентных дзеянняў, нормалізацыя абэктаў абэктавых абэктаў і включэнне механізмаў телеметрыі. Зменшыце колькасць элементаў, каб ўжыванне новай структуры перамагало практыку айхання яе. Перайшліце поступова — апублікуйце кліентскі код, спачатку перанесіце тыя часті, якія маюць найбольшы трафік і ў якіх найбольш проблем з абэктамі, забараніце вжыванне необработанага fetch у новым коде, каб межы перайсця не зменшваліся.

type ApiError =
  | { kind: 'network' }
  | { kind: 'timeout' }
  | { kind: 'http'; status: number; body: unknown }
  | { kind: 'parse' };

export async function apiRequest<T>(
  path: string,
  init: RequestInit & { timeoutMs?: number } = {},
): Promise<{ ok: true; data: T } | { ok: false; error: ApiError }> {
  // timeout, auth, retry, telemetry all live here
}

Няўерны падход. Практыка перайшоўства на новую паляву або такое стрыгучае абгортаванне адпаведзей, што некалькі спецыяльных кэсы застаюцца без обробкі і выкарыстоўваюцься ў чыстам fetch. У обох случаях застаюцца два супернікаючыя шары дадзеных.

Далейшасць. Чырэх месцаў пазней: як правілы лінт-практык і перагляд коду запобегаюць таму, што чысты fetch зноў пачне выкарыстоўвацца?

Мерыябленае. Проектаванне API на рэвэлі групы і поступовае перайшоўства пад тэнсам дзейства.

Заключанне — готавіцеся без запамятоввання

У падготовцы да трывія ёсць меры: запамятовваюцца табелі прымусу, панэлы керавання застаюцца незменнымі без адказаў. У падготовцы да сцэнарыяў ёсць іншы спосаб неудачы: вы вучыце сюжэт, але не механізм яго рэалізацыі. Ўбегайце і тое, і другое, працуючы з рэальным кодам.

Спецыяльна выклікайце гэтыя аберанцы. Адправьце поле пошуку, якое працюе з вялічынай частоты падачы данных у сети, а потым ачыніце карточкі «Працэйнасць» і «Сеть», пакуль не стане явным, што інфармацыя застарела. Залейце адзін інтэрвал без змены, перайдźце на іншую сторону экрана і шукайце адсоўваны элемент у роздзеле «Памяць». Практычныя паходы за дапамою інструментаў развіцця працуюць краща, чым будь-якія прапускі.

Развіцьце навык вимеровання раней, чым шукаце готавыя адказы. Карточкі «Працэйнасць», «Памяць» і «Сеть» у Chrome, а таксама інструмент React Profiler, ператвараюць багато «рангавых» запытанняў у звычныя питанні ўзроўнавання.

Развіцьце навык вголас называць альтернатывы. Практычна ў кожным сцэнарыі існуе больш ад однаго праведзенага рашэння, кожна з якіх мае разныя наследкі. Хорашы спаканавальнікі зазначаюць, чыя рашэнне было спецыяльна выбранае.

Чытайце апісанні інцидэнтаў у працоўным сераверы. Кожны, хто вядома адправляў продукт, ведае пяць такіх сцэнарыёў. Такія історыі кращыя за запознаныя, таму што можна без канца глубэй аналізаваць ўсе деталі.

Зберагаеце короткій список неяўнасцэй з прычынамі та спосабамі ўсунення — це прыміткі, а не портфолія. Калі вас запытаюць пра складную баг, конкрэтная діагназа завжды кращая за загальныя пояснення.

Як у пачаткавіках, так і у аднародных спецыялістаў, патэран павтараецца: называйце спосаб неяўнасці, пропануйце спосаб усунення, який паспраўдзіцца ў даных умовах, і разумейце, што можа зробіць неправильны спосаб усунення прываблівым пад тэнзам. Самэ гэтае і ўважаецца навыком, які на самай працэ прабывання — не ідеальная табелю коерсіі, а здатнасць заставляць системы працаваць чыста, калі сеткі вярбуюць, память зростае, а поставільчыкі не выпалняюць своія зобав’язанні.

Команды, які працюют такім спосабам падчас адзінктування, таксама частае выконваюць калітэксы лепшым чынам: тае ж слоўнасць (рэйсы, thrash, ідэмпотентнасць, радыус уражэння) з’яўляецца як у процесах прабірання на работу, так і пад час аналізу інцыдэтаў. Таму вивучэнне такіх сцэнарыяў дае двойную параду — аднойчы ў кімнате для адзінктування, а ўтрохі пазней, калі панелі керування замарзае на два секунды, а індыкатор застаецца нерухомым.

Адзінктування, якое базуецца на рэальных справах з прыемленаючай системай, таксама паказвае навыкі камунікацыі. Расказ пра рэйс без занадта большай колькасці жаргону, стварэнне простага схематычнага дыяграмы для AbortController або пояснэнне таго, чаму allSettled зменшае радыус уражэння, паказваюць, як чалавек будзе дзейваць пад час інцыдэта. Адказы на трывіальныя запытанні рэдка калі паказваюць такія навыкі, тады як адказы на сцэнарыяў практычна завжды іх паказваюць, і самэ гэта прычына, чаму такі формат продовжае распростраńвацца сярод спецыялістаў середньага та вышэйшага рангу.