За межамі размеру пакета: як з’ясаваць, што насправды спамоўвае ваш веб-дзялоўку
Чаму вычышванне кілабайтаў рэдка калі памагае выправіць медленную аплікацыю, і як відстежваць рэальны час чакання на серверах, у структурах типу «водныя спады», праграмах для гідратацыі, скрыптах трэціх сторон і зображэннях.
У командах, які працуюць з фронтэндам, частае такая ситуацыя: недзелі прабуюць скорачыць размер JavaScript-бандла на 40 КБ, тады калі запит да базы даных, які трывае 900 мілісекунд, застаецца недастрыжаным на критычным маршруце запитаў. Зменяюцца бібліятэкі іконак, заменяюцца залежнасці, насталяваецца іншы плагін для бандлівання, а таксама ведуцься дыскусіяў пра тое, чы розмер пакета становіць 18 КБ чы 12 КБ. Потым хтось запускае прыстаўку на справжнім тэлефоне через справжняю мережу, і ёй заўсёды здаецца, што вона працуе повольна.
У этай статыцэ пояснюецца, чаму розмер бандла так часта становіцься неправым крэтерыем, звядзе гэтае чаканне на самай справе і як запусціць цыкл оптымізацыі, який спачатку усуне самыя большыя проблемы. Меньшыя бандлы дапамагаюць, іноды значна. Але якщо стораніца працуе повольна таму, што чакае на сервер, блакуе рэндарынг, выканае непатрэбную роботу, апрашоўвае занадта многа данных чы запускае вельмі большую структуру компанентаў, то скорачэнне ў 20 КБ не дапаможае яе паспяшыць.
Чаму розмер пакета стаў стандартной метай карыстоцтва
Розмер пакета ўпадзячы, таму што являе сабою цифру. Рэзультаты вашай компіляцыі выглядаюць так:
main.js 842 KB
vendor.js 611 KB
styles.css 94 KB
Хтось прапанавае зменіць розмер JavaScript на менейшы за 500 КБ, і раптам у команды з’яўляецца мета. Яе можна застосавіць у процесах CI, стежыць за ёю праз запрошэнняя змін і святкуваць кожна ўменшэння. Гэта здаецца прагрэсам у інжынерыі, і інодзе так і ёсць.
Проблемы пачынаюцца тады, калі цяперашня цифра перестае быць симптамам і стае самай метай. Команды часта оптымізуюць ту частку карыстоцтва, якую можна найлепша побачыць, а не ту, якая коштуе корыстувача найбольш часу. Чырпак, чаму гэта важна, парабялейце два гіпотэтычныя прыклады програм.
Програма А: маленький пакет, але весь іншы частка працюе повольна
Першая програма выкладзець лёгкі пакет:
JavaScript: 250 KB
Але весь іншы ўсё равно працюе дужа повольна:
Server response: 1.2s
Database query: 700ms
API calls before rendering: 5
Main-thread work: 900ms
Прыемпл A: большы ланч, шырокі шлях да кантэнту
Другі прыемпл перавозіць практычна у тры разы больша колькасць JavaScript:
JavaScript: 700 KB
Але яго сервер і кліент выпрацоўваюць значна меньша колькасць задач прычыну таго, як сторана стане корыстной:
Server response: 150ms
Database query: 40ms
API calls before rendering: 1
Main-thread work: 180ms
Прыемпл A не завжды выграў. Прыемпл у стылі B часта здаецца быстрэйшым, таму што ён выкарыстоўвае прыблізна на секунду меньш часу на сервере, у базе дадзенаў, па серійным запытам і роботу галоўнай вялі, што значна перавышае додатковы час завантажэння для большасці корыстнікаў з нормальным з’яўленням. Прынцып, на які трэба асабліва зважаць пад час обгаворэння прыдатнасці, ёсць просты: корыстнікі не спрыймають кілабайты, яны спрыймаюць час чакання.
Завантажэнне стораны — це дзяўгі процес, а не тры крокі
Багато разработчыкаў маюць у свайных галавах спрасаваную модэль завантажэння:
Download JavaScript
↓
Execute JavaScript
↓
Page appears
Рэальны запыт праходзіць через значна больш заўсёды, кожны з якіх можа спамтаць:
DNS
↓
Connection
↓
TLS
↓
Request
↓
Server processing
↓
Database
↓
Response
↓
HTML parsing
↓
CSS processing
↓
JavaScript download
↓
JavaScript parsing
↓
JavaScript execution
↓
Hydration
↓
API requests
↓
Rendering
↓
Layout
↓
Paint
Пошук DNS, налагоджэнне з’яўлення звязку і пераговоры па протаколу TLS адбываюцца раней, чым сервер бачыць хоць шта. Пасля гэтага прыходзіце обробка дадзеных на серверы і робота з базай мэтаў. Браузер пасля гэтага парсуе HTML, обрабоцвае CSS, завантажае, парсуе і выконвае JavaScript, адбываюцца вызовы API, і толькі пасля гэтага відбываецца атрыбутаванне, распаковка і адрасаванне элементаў. А калі корыстнік нажме на елемент, вялікая частка гэтага цыклу павторюецца.
З усімі гэтымі этапамі пакет коду ёстся там, дзе час можа зникнуць. Фраза «Зменшыце размер пакету» є некальканая, таму што яна прыпускае вяснаванне рашэння, як толькі не ведаеш, дзе саме зникае час.
Сервер можа быць самай повольнай часткай вашага фронтэнду
Інжынеры фронтэнду прыродна спрыяюць да таго, што выконванасць спрыяючы працэй браузера. Вы ачынаеце DevTools, пераглядаеце вкладку Network і часткі коду на JavaScript, а таксама запускаеце Lighthouse. Але значны частак вярбованай швальнасці фронтэнду вялікі ўжо да таго, як браузер атрымае ўсё неабходнае.
Возьмімо запит да панелі керавання:
GET /dashboard
За ўсім гэтым сервер можа выконаць усё гэта прытаму да адпаведзення:
→ authenticate user
→ fetch organization
→ fetch permissions
→ query projects
→ query project statistics
→ query notifications
→ calculate recommendations
→ render response
Якщо весь гэты процес займае 1,4 секунды, змена розмеру пакета з 600 KB на 500 KB ледзь вплывае на ўжытак, таму што першыя значныя байты прыходзяць усё роўна за 1,4 секунды. Браузер не можа атрымліць і адобразыць данні, якія ён не атрымаў.
Частым прычынам являецца код бэкэнду, який чакае на аднойчыны завершэння розных операцый. Усё пачынаецца з запрашэння інфармацыі пра корыстніка:
const user = await getUser();
і продовжаецца ланцюгам дадзейнаў wait() для організацыяй, проектаў і паведамленняў, пры чым кожны чакае, пакуль завершыцца папярэдні:
const organization = await getOrganization(user.orgId);const projects = await getProjects(organization.id);const notifications = await getNotifications(user.id);
Дзеянні з іх часткай сапраўды залежныя: для пошуку організацыі патрэбны user.orgId, а проекты — ідэнтыфікатор організацыі. Але тое, што не залежыць ад папярэднега рэзультата, без прычыны падлежыць серійнам затрымкам. Калі операцыі незалежныя, ўзначальванне іх адразу і чаканне на яны разам можа значна скорачыць час адпаведзі:
const [user, notifications] = await Promise.all([
getUser(),
getNotifications()
]);
Заўважыце, што паралельная версія вызывае getNotifications() без ідэнтыфікатора пользователя. Гэта можліва толькі тады, калі паведамленні можна атрымаць з чаго-небудзь вялікай доступнасці, напрыклад з сесіі; інакша гэты вызов все равно мусі чакаць на пользователя. Загальныя правілы — адобразіць рэальную графіку залежнасцяў і запускаць кожны їё рэвень паралельна. Чырпак больш па выборе між гэтымі патэранамі можна знайсці ў нашым кяле Promise.all, Promise.race і sequential awaits. Такія змены могу легка перавыпрацаваць будзь-якую роботу з бандламі.
Модель «водоспада» коштуе больш, чым просто байты
Аднам з найэфектывнейшых месцаў для пачатку расследавання є вікно «Сеть», а не аналізатор бандлаў. Дужа распашчасты патэран выглядае так:
HTML
↓
JavaScript
↓
API A
↓
API B
↓
API C
↓
API D
Кожны крок чакае на пярэдні, і кожная «стрэлка» дадае додатковы час адчакання. Порашчыце гэта з дизайном, у яком першая адпаведна інфармацыя вже містіць усе, што патрэбна сторанцы:
HTML
↓
API response containing everything required
Другая версія можа перадаваць большы калекцыю байтов, пры тым застаўаючыся значна быстрэй. Калекцыя байтов і час адчакання — гэта разны проблемы. Адпаведна інфармацыя размерам 100 КБ, якая прыходзіць за адну моментчыку, можа перамагаць інфармацыю размерам 20 КБ, якой патрэбны чатыры последовальных раунд-трыпа, прычыму сторанца не зможа выконваць нічога практычнага, бясконечна больш на мобільных сетках, дзе кожны раунд-трып є дужа дорогі.
Таму, калі вы бачыце, што канечная тэрміналавая точка вяртае 300 КБ, першы рэфлекс — зменшыць размер пакета даных. Гэта можа быць корисна, але лепшы першы вопыт — гэта чаму ўжо самай пачатку корыстніку патрэбна гэта адпаведна інфармацыя, каб ён мог узаемадзейваць з сторанцай. Адпаведны адказ часта паказвае задачы, якія можна адкласціць на пазнейшы час або аж увесь час зняць.
Робота JavaScript мае большэйшую значымасць, чым яго размах
Адна з супарадных пастак — сплутванне размаху вашага JavaScript з работай, яку ён выканае. Бандл размахам 500 КБ не ўсё час являе сабою катастрофу. Гэта, што мае значэнне, — гэта тое, што павінен зрабіць браузер з яго:
- З’явіць яго.
- Распакаваць яго.
- Компіляваць яго.
- Выканаць яго.
- Стварыць стан прыкладнай програмы.
- Склadaць дрэвы компонентаў.
- Прыўяжыць обробнікі змаганняў.
- Актуалізаваць маркап, выробленаў на серверы.
- Перасчытаць макет.
- Атрыбуець рэзультат.
Два даплы з падобнымі размарамі пакетаў можу значна разліквацца за вартасцю выканання. Узглядзім таблы з 5 000 рядоў. Самыя даныя рэдка калі ёсць проблемай; зазвычай проблема крыўціцца у адрасаванні DOM-вузлаў для 5 000 інтэрактывах рядоў. Рашэння не ў тым, каб скорачыць 50 КБ коду, а ў тым, каб адрасаваць толькі тыя 30 рядоў, якія зараз відображаюцца. Гэты метод — віртуалізацыя — дазволяе застаўляць той самы дапл і тыя ж даныя, адночасна потэнцыйна зменшыўшы частку роботы браузера.
Калі гідратацыя стае вузкам местам
Гэта разлічэнне ўсё бол выражанае у React і іншых фрэймворках компонентаў, якія адрасаваюць на серверы. Адрасаванне на серверы дазволяе быстра прадаст HTML на экран, але браузер можа знадобіцца гідратаваць вялікі дрэво компонентаў, прычаму нічога не рэагуе на ввод:
HTML arrives quickly
↓
User sees content
↓
Browser starts hydration
↓
Large amount of JavaScript executes
↓
Page becomes interactive
Старонка выглядае готовайцей задоўж часу, пакуль насправды не будзе гатовая. Самэй таму меркаванне толькі тады, калі контэнт пачынае ап’являцца, можа вас заплутаць. Дашборд з 200 інтэрактыўных компанентоў можа стварыць абліковае HTML і ўсё ж займаць значны час CPU падчас процесу гідратацыі, залучаючы клікі без адпаведнай рэакціі.
Корыстны вопыт тут не ў тым, чы роўнайна пакет занадта вялік, а ў тым, чаму так много коду павінна стаць інтэрактыўной адразу. Дзеякія компаненты можа вовсе не патрабаваць кліентскага JavaScript. Дзеякія інтэракцыі можна выдзельваць у маленькія блакітнікі. Дзеякія віджэты можна завантажваць пазней, а дзеякія компаненты, выраблены на серверы, можа вовсе не патрабаваць процесу гідратацыі. Такія тэхнікі, якія аналізуюцься ў нашам адглядзе частковага прадзеявлення і адночаснага дзеявлення, даюць набліжна большыя рэзультаты, чым проста спарчванні ўжо па 30 KB залежнасцяў.
Скрыпты трэціх сторон частаюць значнае вантажы для вашага сэрвара
Перад запускам кампаніі большага павелічыню, пераканайцеся, якую частку коду, які вы адправляеце, напісаў кто-небудзь іншы. Тыповыя прыклады:
- аналітыка
- віджэты для чату
- хітмапы
- тэставанне типу A/B
- распрацоўка рэкламы
- інструменты падтрымкі клёяўцоў
- запіс сеансаў
- вбудоўкі у соцыяльныя мераслужбы
- маркетынгавыя пікселі
- карэнтуле сагласнасці
Кожны з гэтых элементаў можа дадаць запыткі, выкананне скрыптаў, работу з макетам і сецыянарную актывацыю. Іронічна, але гэтыя скрыпты часта не прабываюць перакананыя, тады калі інжынеры выдзейваюць гады на налагоджэнне коду прыкладнення. Старонка можа заваносіць такі набор:
app.js
analytics.js
chat.js
tracking.js
experimentation.js
heatmap.js
Команда аплодуе змене розміру файла app.js на 70 КБ, хоча стораннія частка коду залишаецца у розміре сотняў кілабайт. Самэлькі ў такім разе бюджеты на павышэнне працоўнасці трэбуе расширваць. Уместа таго, каб спытацца, які розмер мае ваш пакет коду, трэба спытацца, сколькі коду павінен обрабацваць прыстрой корыстувальніка, перш чым стораннае будзе корыстным. На этыя два запитанні є абсалютна разныя адпаведзенні.
Аўтарыфакты можу перавышыць усі ваашы бюджеты на JavaScript
Занадта вялікія аўтарыфакты ёсць яшчо аднай проблемай, якую часта ігнаруецца. Адзін галоўны аўтарыфакт можа перавышыць розмер усіх оптымаўзаваных частак JavaScript:
main.js 180 KB
hero.webp 1.4 MB
product.jpg 900 KB
background.png 2.1 MB
Якщо патракцыя, якая выкасывае залежнасць розміром 12 КБ, будзе прийнята, а фонавы аўтарыфакт розміром 2,1 МБ застанецца без змян, команда проста выконвае формальнасці параджэння задач, а не ўжо справжню оптымаўзацію. Аўтарыфакты заслуговуюць на тое жа рэгулярнае ставленне, што і код:
- Вярніце прыоритэт савременным форматам, такім как WebP або AVIF, калі яны падтрымваны і паслядовыя.
Зямлеўванне 15 КБ коду JavaScript мае малая значыць, якщо тэлефон усё равно заўантажвае 2 МБ для зображэння, якое корыстувача ледзь беліце.
Большасць проблемаў з выдатнасцю — это проблемы архітэктуры
Чым глęбей аналізаваць, тым ясней стае, што багато проблемаў з выдатнасцю ўжо не стосуюцца налаштавання коду. Яны вынікаюць з таго, як структуравана сама аплікацыя. Уявіце сторанку продукту, якой патрэбны ўсе гэтыя элементы:
Product
Reviews
Recommendations
Inventory
Shipping estimate
User preferences
Related products
Якщо кожны элемент запрашываецца окалічна пасля завантажэння старонкі, можна оптымізаваць кожны запит, але старонка застаецца медленной. Кращы падход — спачатку выбраць тое, што ўжо трэба паказаць корыстніку. Пачатковая адпаведзь можа складацца толькі з:
Product
Price
Availability
Primary image
Атрыбуты, рэкамендаціі та супаўзвязаныя продукты могу прыходзіць пазней. У такі момент вы вже не оптымізаваеце рэалізацыю; вы перазначаеце, што значыць „гатовасць“ для старонкі, і самэль тут часта даўаецца найбольшы эфект.
Зьвярзацца з паметкамі, якія фактычна бачыць корыстнікі
Серйозная праця над аддачай заменяе пытанне „насколькі велик пакет?“ на пытанне „колі корыстнік зможа зрабіць ўжо каліснае?“ Этае пытанне прыводзіць да лепшых показнікаў.
Час да першага каліснага контэнту
Калі корыстнік бачыць тое, за чым ён прыйшоў? Гэта часта залежыць ад вашага продукту: залишак на рахунку, результаты пошуку, фота продукту.
Час до інтэракцыі
Калі корыстнік можа надзеявана весті інтэракцыю, калі клікі не з’яўляюцца ўнаследак іншых задач? Няўясковыя версіі Lighthouse больш не включаюць показнік TTI у свой рэйтынг, але сама проблема застаецца важлівай для стежэння за вашымі сторанкамі.
Сяродзеўны час атрыбутавання контэнту
Калі завершаецца атрыбутавання главнага видимага контэнту?
Час ад інтэракцыі да наступнага атрыбутавання
Як шыбка інтэрфейс реагуе візуальна пасля таго, як корыстнік весты інтэракцыю?
Аб’ёмныя зсувы лейауту
Чы характар лейауту змінюецца, калі корыстнік намагаецца прачытаць або клікнуць?
Аб’ёмны час блакавання
Як дзяўна главная вясь блакуецца з-за задач, якія не дазволяюць браузеру реагаваць на ввод корыстніка?
Ні адно з гэтых пунктаў сама по сабе не раскрывае всю картыну, але разам яны описваюць практычную ситуацыю набліжна краща, чым адзін-едын лінкія падобнае гэтаму:
bundle.js = 487 KB
Фігура пакета описвае актыў. Памэтры карыстоўчыкавага дасвіду паказваюць, калі саме він страждае.
Цікавы для выкарыстоўвання цикл оптымізацыі
Калі неабходна паспрабаваць вылечыць медленную прыкладненне, выдалэнне залежнасцяў не павінна быць першым крокам. Болей эфектыўны є дисцыплінованы цикл.
1. Павтарыць тэсты за реалістычных умоў
Тэставаць трэба на репрезентатыўных прыстроях і сетях, а не толькі на швайнай лэптопе через офісны Wi-Fi. У багатох вашых карыстоўчыкаў няма ні таго, ні другога.
2. Замеры для выявлення прычыны медлівасці
З’ясаваць, дзе губіцца час. Чы ўсё гэта є прычыной медлівасці?
server response?
network?
rendering?
JavaScript execution?
layout?
images?
third-party scripts?
3. Аднакова выявіць галоўную прычыну
Не трэба атакавацца на вылечыць пяць проблем адразу. Неабходна знайсці самую значныю прычыну.
4. Змяніць адную прычыну
Зрабейце самыя маленькія архітектурныя чыстаўкальнія змены, якія паспрацуюць проты гэтага узгорнутка, каб можна было прысваяць рэзультаты конкретным дзеянням.
5. Звярніцеся да памероў знову
Якщо паліпшэння не праказуецца ў цыфрах, не прыпускайце, што ён запрацаваў.
6. Забезпечыце стойкасць паліпшэння за дапамогою перагляду регрэсіі
Паліпшэння быстра зникаюць. Хтось дадае залежнасць, каманда разработчыкаў дадае віджэт, компонент стане менш эфектывным, запыт перацягнёцца у серійную форму, і чырэх месцаў пазней вы знова будзеце там, дзе і былі. Для падтрымкі выдатнасці неабходныя автоматызаваныя механізмы контролю ў процесах CI і монітарынгу, а не эпізодычныя спробы якога-небудзь паліпшэння.
Размер пакета заўсёды мае значэнне, проста ў іншай форме
Нічыя з гэтых прычын не робіць размер пакета незначным. Вялікія пакеты падвышаюць витраты на завантажэння, аналіз, кампайляванне і выконання, пры чым наследкі ўсё болей сутэчныя на повольных прыстроях і сетях. Раздзелэнне коду, аптимізацыя структуры, паслядовае завантажэнне і адзялэнне непрыдатных залежнасцей — усе гэта мае вялікую цэннаць. Ключовая ідея — прыменяць гэтыя методы тады, калі доказы паказваюць, што гэтыя проблемы є найбольшымі.
Здаровая аналіза прыдатнасці, якая ведэцца па порядку ўплыву, можа выглядаць так: спачатку — повольна адпаведзь сервера:
Problem:
900ms server response
Выправлена праз паралельную роботу запытоў бэкенду:
Action:
parallelize backend requestsResult:
-420ms
Далей — дорогі процес заполнення панелі керування:
Problem:
large dashboard hydration
Рашана праз адкладэнне запуску компонентаў, якія не патрабуюць негайнай інтэрактыўнасці:
Action:
defer non-critical interactive componentsResult:
-280ms main-thread work
Потым — занадта вялікіе зображэння:
Problem:
hero image is 1.8 MB
Рашана праз аддачу ў форматах, прыстосаванных да разных дзвяроў, у сучасных форматах:
Action:
responsive WebP/AVIF deliveryResult:
-1.2 MB transferred
Толькі пасля всього гэтага ў верхней частыні спісу з’являецца значныя залежнасць ад JavaScript:
Problem:
large JavaScript dependency
Їх замена такі ж дапамагае заўсёды значна:
Action:
replace dependencyResult:
-60 KB
Гэтыя правкі таксама ўжо ценныя. Яны проста патрабуюць чэтвёртага месца ў черзі, а не першага.
Ключовыя выводы
- Оптымізавайце час чакання, а не розмер файла. Размер пакета — толькі адны з многіх показначыкаў.
- Аналізавайце сервер і процес обробкі запита ранейш, чым аналізаваць пакет; затрымкі і колькасць перадач данных часта маюць большыя наследкі, чым проста розмер файла.
- Оцінюйце JavaScript па роботе, яку ён выканае: парсінг, выконанне, атрыбутызацыя і іншыя процесы, а не толькі па його розмеру.
- Аудытуйце скрытые скрыпты і з’явы трэціх сторон з той жа строгосцю, як і ваш сэнсабльны код.
- Перазначайце значэнне „готовасці“ для кожной сторонкі, ўжыткам чаго критычны контэнт будзе падаць першы, а ўсё інше — пасля.
Спаднёе чытанне
- Што на самай працоўнае робі фронтэнд-разработчыкаў ценнымі ў эпоху AI — Пасвячана таму, чаму розумэнне, суджэнне і мышленье на рывень системы зараз маюць большое значэнне, чым вольнае володзення фреймворкамі, калі AI перэймае рутынную фронтэнд-разработку.
- За межамі P95: зьмерэнне латэнсу, які на самай працоўнае перажываюць вашы корыстнікі — Чаму здаровы показначык P95 можа існаваць разам з медленным продуктом, як час чакання ў черзі і распространэнне запытаў захоўваюцца ад дашбордаў, і як зьмерэнне часу за кожны крок паканчыць гэру звінавачэнняў у латэнс.
- Што JSON.stringify таямніча паскаў, ператварае і адмовяецца серыяваць — Дазвольце вам дазнацца, якія значэння JavaScript JSON.stringify прыхоўчвае або зменяе, як функциі toJSON, заменнікі і функцыі вярнэння дапамагаюць выправіць гэта, і калі structuredClone є лепшым адпаведнікам.
- Месца запісу функцыі вялікай значэнні: лексычны дыяхронум JavaScript — З’ясавайце, як JavaScript разв’язвае імёна змэнных через лексычныя среды, чаму месца вызову ніколі не мае значэння для пошуку, і як гэта паўтарыцца ў хендлерах React.
- Што оптымізуе компайлер React і шта застаёцца на вашай адпаведнасці — З’ясавайце, якія задачы з парадуксамі выкааноўвае автаматычна компайлер React, чаму спадзяльныя API і важкія пакеты застаюцца вашай адпаведнасцю, і як безбедна ўжываць яго ў вялікім кодбазе на базе React.
- Раскрыць таінства Virtual DOM Diffing: Што поручае React і чаму гэта корыстна — З’ясавайце, што на самае ў рэальнасці ёст меркавы DOM у React, як процес супраўляння поручае два дрэвы элементаў, якія змянення відбуваюцца на стадыі фіксацыі, і звядзе гэта з падышчама ў карыстнасці.
- Reflow, Repaint, Composite: Колькіх ресурсаў спрацоўвае браузер на кожную змяну CSS — Дакладна разберамся, як HTML і CSS працуюць у DOM, CSSOM, пад час форматавання, аплявлення і складання элементаў, і дазнаюся, чаму змяны шырыні коштаюць браузеру больш ресурсаў, чым змяны кольору, а таксама як утримаць стабільнае форматавання.