Галоўная / Артыкулы

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

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

Выдавец: REACTAPP.TOP20 хв чытання4513 слоў

Крыніцы: JavaScript Interview Questions Based on Real Production Scenarios

Адаптаванае з 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 зменшае радыус уражэння, паказваюць, як чалавек будзе дзейваць пад час інцыдэта. Адказы на трывіальныя запытанні рэдка калі паказваюць такія навыкі, тады як адказы на сцэнарыяў практычна завжды іх паказваюць, і самэ гэта прычына, чаму такі фармат продовжае распростраńвацца сярод спецыялістаў серэдньага і вышэйшага рангу.