2021 год. 4к и 8к трансляции уже не новость. Ryzen выпустил 64-ядровый процессор. Наконец-то все забыли об оптимизациях в вебе, потому что это сложно, дорого, и попросту уже не нужно.
Если вы думаете именно так, мне есть что вам сказать.
Давайте начнем с простого. Конфигурация ноутбука, с которого я пишу эту статью — Сore-I5 7200U (2.5GHz-3.10GHz), 12GB оперативной памяти, SSD + нечто вроде 100 МБит сети, если мой провайдер не врет. И, честно говоря, я не часто вижу совсем уж медленные сайты и веб приложения, когда серфлю в интернете. Несмотря на это, каждый раз, когда я пишу код, я трачу некоторое время на обдумывание того, насколько сильно мои изменения повлияют на моих пользователей. И это вовсе не потому, что я такой красивый, а потому, что согласно данным аналитики, большинство клиентов используют наше веб-приложение с помощью мобильного телефона. Что, в свою очередь означает нестабильное хG соедениение (где x находится в пределах 2–4), меньший объем памяти (1–4 GB вместо моих 12) и процессор с условной частотой в 1.7GHz — 2GHz который и так чуточку занят десятком-другим вкладок и пятеркой приложений. Поэтому то, что у меня работает нормально — может подтормаживать и раздражать наших пользователей. А учитывая размер минимального чека моего пользователя — я очень не хочу никого раздражать. Да и по-человечески просто приятно, когда твое приложение работает быстро.
Именно поэтому, каждое код-ревью, я обращаю внимание на разные моменты связанные с производительностью. Моя команда — большие молодцы, потому что они терпят мои замечания и даже иногда с ними соглашаются. Но, естественно, я общаюсь не только со своей командой, и часто, когда я говорю, что производительность в вебе все еще важна, в ответ я слышу аргументы, о которых уже упоминал — поддержка, цена работы и возросшая вычислительная мощность. И вот о них я хочу поговорить.
640kb хватит всем
Безусловно, мощность современных вычислительных систем просто поражает. Процессор моего смартфона в 16,(26) раз быстрее процессора моего первого компьютера с которого я вышел в интернет. И это, не говоря уже о том, что там было только одно ядро, а в телефоне их целых четыре плюс четыре. А ведь сайты по большому счету не изменились. HTML для содержимого, CSS для красивостей, и JS для взаимодействия. Из серебра не родилась молния, флеш не быстро, но тоже похоронили, а WebAssembly все еще в сборке. Да и пропускная способность современных сетей позволяет больше не требовать формат изображения, который бы рендерился снизу вверх. Расходимся? Увы, но нет.
В 2019 году, была опубликована любопытная статья, авторы которой утверждают, что первые пять секунд являются ключевыми, если вы хотите добиться высокой конверсии.
К выводам есть некоторые вопросы (в частности, очень любопытны причины роста конверсии на 6 и 9 секундах), но в целом идея логична. Чем дольше пользователь ждет контента, тем выше вероятность того, что он уйдет. За два года до этого, в далеком 2017, другая группа исследователей опубликовала еще одну статью согласно которой, для того чтобы успеть показать контент в первые пять секунд, размер загружаемых ресурсов не должен превышать 170KB (уже сжатых), если JavaScript-а "немного", или 130KB для сайтов/приложений построенных с помощью JS фреймоворков. Причины были названы следующие:
- 45% мобильных соединений используют 2G
- 75% всех соединений используют 2G или 3G
- Усредненный телефон — Motorola Moto G4 с процессором Octa-core (4x1.5 GHz Cortex-A53 & 4x1.2 GHz Cortex-A53)
В таких условиях разгуляться сложно, поэтому бюджет и выглядит так скромно. Однако с тех пор уже прошло четыре года и в поэтому в 2021 году исследование было повторено. Ситуация действительно улучшилась и теперь у нас есть 100Kb для HTML/CSS/Fonts и 300-350KB для пожатого JavaScript. Однако это произошло в основном благодаря улучшению качества связи. Вычислительные мощности устройств изменились не очень сильно. Взгляните на этот график и оцените прогресс с 2017 года.
iPhone показывает стабильный рост, а вот остальные производители нажимают на педаль газа весьма осторожно. Но не iPhone-ом единым жив мобильный веб. Бюджетный сегмент ноутбуков тоже "не блещет". Вот несколько представителей в сегменте за 350 долларов:
Первая модель — всего 4GB DDR4, двухъядерный Intel Celeron N4020 (1.1–2.8 ГГц). Наверняка можно найти что-то и получше на этот бюджет, но если такие модели продаются, значит это кому-нибудь надо? А ведь еще есть огромное количество просто устаревшей, 10-и и даже 15-летней техники в офисах и домах. Можно сказать, что, игнорируя производительность своего сайта или приложения, добровольно отказываемся от части пользователей, и делаем жизнь остальных чуточку сложнее. Что и сказывается на конверсии.
Поэтому оптимизация ресурсов с точки зрения размера начального бандла — все еще чрезвычайно важна. И меня сильно огорчает, когда, опытные разработчики, в обучающих статьях про, например, логгирование (а не про Moment), пишут нечто вроде такого:
import * as moment from "moment";
//...
const date = moment();
const requestDuration = moment().diff(startMoment, "milliseconds");
Конечно, я все понимаю. Скорее всего в проекте у автора уже стоит moment, он к нему давно привык, поэтому он его и использовал. Но… это же учебный пример. Кто-то не посмотрел, как всегда, не подумал, скопировал и вот вам, в ваш любимый проект влетает от 18KB до 72KB JavaScript (и это уже после сжатия). А ради чего? Ради единственного метода diff
. А потом, людям приходится даже писать специальные плагины чтобы как-то это оптимизировать. И вот таких примеров, без предупреждений, без сносок о том, почему это может быть плохо — легион. Авторы статей, блогов и курсов, в погоне за простотой кода и аудиторией, приучают других разработчиков игнорировать производительность как класс.
А ведь помимо простого размера есть много других нюансов. У нас еще есть стили, которые парсятся хоть и быстро, зато умеют блокировать JS. Есть еще шрифты, которые могут заставить браузер вообще не показывать текст, пока не загрузятся. А самое мое любимое, так называемый холодный старт TCP, который просто игнорирует весь ваш 100МБит канал и позволяет отправить первый пакет не более чем в 14KB.
Поэтому, несмотря на весь прогресс, если мы не хотим терять/раздражать среднего пользователя, наш бюджет все еще довольно ограничен. Но, загрузка — ведь это тоже еще не все. Для SPA приложений просто загрузить ресурсы мало. Еще нужно выполнить JavaScript, и только потом, основываясь на вычислениях, отрисовать пользователю какой-то контент. На слабых устройствах это тоже будет вызывать проблемы. К примеру, вот сравнительный график загрузки нашего SPA приложения, написанного на React, без замедления CPU и с 6-кратным замедлением.
Общий объем загруженного JavaScript — 253Kb, что не так и много. А время работы скриптов увеличилось с 362 миллисекунд до 2102 миллисекунд, т. е. в примерно в 5.8 раза. Largest-Contentful-Paint (не лучший ориентир, но подойдет) сдвинулся с 2.2 секунд до 4.7 секунд и немного подросли другие метрики.
Хотел еще так же померять Хабр, но там сейчас какая-то совсем безумная мешанина скриптов для аналитики, пришлось отказаться. И, к тому же, для статики и рендера на стороне сервера (как раз наш Хабр) CPU менее важен, браузер DOM строит довольно быстро, если ему не мешать. Но если совсем интересно — вот моя статья как раз про Хабр.
Почему это важно? Потому что JavaScript язык в основном однопоточный и интенсивное его выполнение, блокирует основной поток и останавливает отрисовку HTML. Простыми словам, while(true){}
установленный где-то в начале документа убивает все настолько качественно, что вы не то что на кнопку нажать не сможете, вы даже статический контент после этого кода не увидите даже на каком-нибудь Фугаку. Конечно, в здравом уме бесконечный цикл писать никто не будет, как и синхронно загружать его в страницу, но способов выстрелить в ногу в современном вебе все еще остается предостаточно. Тут и последовательные запросы вместо параллельных, и N+1 прямо с фронта, и тяжелые regex-ы во время рендера приложения, и чрезмерная работа с DOM-ом и многие, многие, многие. Да и сам фреймворк может вам подкинуть задачку.
Например, для меня до сих пор остается загадкой, почему, по умолчанию, функциональные компоненты в React вызываются всякий раз, когда вызывается их родитель. Да, это легко поменять, использовав React.memo
или перейти на PureComponent
, но библиотека, которая позиционируется быстрой (или уже нет?), почему-то решила пойти именно таким путем. Особенно проблемным это стало после популяризации хуков, что привело к усложнению функциональных компонентов. Кстати, раз уж речь зашла про React — вы конечно же знаете, что React встраивает изображения до 10kb в JS код?
Правда, если вы думаете, что у Angular с этим всем легче, то — не совсем. Утечки памяти из-за сложности Rx.JS, постоянный перерендер компонентов из-за дефолтной Change Detection Strategy и многое другое приводит к тому, что некоторые вообще отключают NgZone.
Резюмируя сказанное: несмотря на то, что мой смартфон может просчитать траекторию полета Теслы на Марс, с отображением «простого текста» у него могут быть проблемы.
Теперь перейдем к остальным аргументам.
Оптимизированный код сложнее поддерживать
В качестве обоснования этого аргумента часто приводят нечто вроде вот такого примера:
Нормальный код:
const names = users
.map((user) => user.name)
.filter((name) => name[0] === "a")
.join(" ");
Оптимизированный:
let names = "";
for (let i = 0; i < users.length; i++) {
const userName = users[i].name;
if (userName[0] === "a") {
names += `${userName} `;
}
}
const result = names.trimEnd();
— Мол, что тебе легче читать и поддерживать?
Конечно, я отвечаю, что первый читается легче и мой оппонент радостный уходит в закат. Но давайте будем справедливыми — не все оптимизации выглядят именно так (хотя если у вас миллион пользователей, то и такому вы будете рады). Например, вот ошибка, которую я периодически встречаю при отображении полностью статических компонентов:
const TermsAndConditions = () => (
<ul>
{["Don't be evil"].map((term, i) => (
<li key={i}>{term}</li>
))}
</ul>
);
А это его совсем чуть-чуть "оптимизированная" версия:
import { rules } from "./settings";
const TermsAndConditions = () => (
<ul>
{rules.map((term, i) => (
<li key={term}>{term}</li>
))}
</ul>
);
Скажите, разве это плохо (~С)? Я бы так не сказал. Зато теперь у нас статический контент лежит в настройках, а бонусом мы не создаем новый массив на каждый вызов функции (а это время на аллокацию и время на уборку мусора). Конечно, можно и лучше "оптмизировать", но даже это лучше чем было. Спички, скажете вы? Спички, да. Но что насчет вот такой популярной ошибки?
Было:
const user = await fetch(userId);
const car = await fetch(carId);
Стало:
const userRequest = fetch(userId);
const carRequest = fetch(carId);
const [user, car] = await Promise.all([userRequest, carRequest]);
Код не стал хуже. Просто теперь, где-то пользователь будет ждать немного меньше, а нам это почти ничего не стоило (кроме полифила для IE). И таких мест может быть много. Чего стоит один code-splitting. Почти никакой магии и куска бандла как небывало. Его больше не надо грузить, парсить, и вы ничего не потеряли — все просто стало немного быстрее. Или пресжать бандлы, чтобы ваш сервер этим не занимался на лету. Тоже спички, но тут даже код менять не надо.
К чему я веду — код, который не написан чтобы быть медленным, вполне может работать быстро. Звучит как призыв Капитана Очевидность, но часть проблем с производительностью я исправлял именно так — не ускоряя код, а просто устраняя медленный. Возможно, с этим подходом вы и не попадете в топ 1 самых быстрых сайтов или приложений, но, как известно, 20% усилий приводят к 80% результату, а именно это нам и надо.
Мы не можем тратить на это время, нам уже в продакшн надо
И, наконец, последний пункт. Многие считают, что оптимизации производительности — это дорого или по времени (долго, клиент не выделит) или по деньгам (нужен спец, бюджета нет, когда-нибудь потом). И это, на самом деле, самый болезненный момент. Если беклог выше Фудзиямы (куда-то меня на восток потянуло), релиз завтра, менеджер пингует каждые два часа с прекрасным "ну как там", заниматься оптимизацией вам будет тяжело. Но есть два "но", которые я хочу проговорить.
Во-первых, многие "оптимизации" — это вовсе не оптимизации. Это просто корректно организованный код, который не тормозит. Да, есть сложные/не очевидные моменты, особенно на стыке приложение/инфраструктура, но, если мы будем грамотно использовать ресурсы, которые у нас есть, все уже будет гораздо лучше. В конце концов, JavaScript — это основной инструмент в веб-разработке и понимание того, что const really = () => ({})
создает в памяти новый объект на каждый вызов, должно быть очевидно даже коту фронтенд программиста.
А во-вторых, (внимание, холивар) объяснять заказчику или ПМ-у, что производительный веб нужен в первую очередь проекту — это именно наша задача как специалиста. Я понимаю что вы можете со мной не согласиться, но заказчик-то вообще не знает, что такое эти ваши FCP, LCP, TTI, не знает, что можно уменьшить latency на 0.3 секунды и заработать на этом 8_000_000 фунтов стерлингов в год. У него в голове может просто не быть понимания того, что быстрый веб — это точно такая же фича как, например, список рекомендуемых товаров. Почему? Потому что они оба напрямую влияют на продажи, а значит и на доход. И вот этот аргумент заказчик понимает прекрасно.
Так что же делать
Как ни странно, но я не предлагаю тут же бежать, удалять проект и все переделывать. Я также не призываю душить ПМ-а или продакта. Скорее всего это не сработает. Вместо этого я предлагаю вам, когда будет немного свободного времени, выбрать ключевые метрики производительности вашего приложения и просто начать их мониторить. А о результатах информировать лиц, принимающих решения. Если у вас на проекте все хорошо — вам скажут, что команда молодцы и вы со спокойной совестью и документальным подтверждением своих прямых рук пойдете пить нефильтрованное. А если плохо — скорее всего ПМ/Продакт сами придут к вам за решением проблемы и тогда уже вы будете говорить о сроках и объемах.
Ну а о том, как измерять есть не одна статья. Например, можно поставить в пайплайн lighthouse и замерять производительность прямо во время сборки/тестов. Можно на крон повесить ежедневные тесты прода с тем же lighthouse или perfrunner. Кстати, оба инструмента позволяют вам задать и параметры сети, и замедление процессора, чтобы понимать, как покажет себя ваш вебсайт в более сложных условиях. Можно подключить PageSpeed Insights. Можно даже написать свои тесты производительности на голом puppeteer, и мерить вообще все что хотите и как хотите, это тоже не так сложно. Главное, если ваш основной рынок это, например, Северная Америка, то не пытайтесь тестировать ее из Киева, может неловко получиться.
Послесловие — преждевременная оптимизация — корень всех зол
Цитата из заглавия известна всем и часто приводится как 0-й аргумент против оптимизации веб приложений. Но с вебом есть один не очевидный нюанс. В вебе пользователь не жалуется, а просто молчаливо уходит. Поэтому, если вы не хотите остаться без пользователей, то оптимизацию я бы начал все-таки пораньше, во избежание. И тогда все будет хорошо.
Надеюсь, было полезно, всем доброй ночи.
amarao
Автор сайта на js, пишущий, что у кого-то там компьютер со "всего" 4ГБ памяти, о которых "тоже надо думать", вероятнее всего, просто не понимает как это много 1ГБ.
Я среди всего веба могу назвать всего несколько сайтов, которые заслуживают так много памяти. Это интерактивные приложения с сотнями и тысячами элементов.
Если вы делаете "новостной сайт"/"интернет-магазин"/"ещё что-то с гамбургером (меню)", то я не понимаю о каких гигабайтах идёт речь. Отдайте скромный html (сколько у вас там контента? 5кб? 15? Ваш копирайтер сильно старался и там 40кб текста? Аж 60кб в HTML?), может быть ваш скромный JS (килобайт? Полтора?).
Зачем вам 300кб JS'а? Вы свою операционную систему хотите загрузить? Или у вас такая невероятно глубокая node_packages, что даже нахождение в одной комнате с ней порождает 500кб JS'а на каждый вздох?
Drag13 Автор
Автор понимает, у автора первый винт был размером 65МБ. А до этого были просто дискеты.
И автор с вам согласен что 4 ГБ это безумно много. Только вы тоже забываете, что 4ГБ это не эксклюзивно. Там винда еще, сам Хром, еще какие то резидентные приложения. и вот из 4ГБ у вас уже 0 + свап файл. А JS-у жить как-то надо.
В остальному соглашусь.
Aleksei_Segodin
Какой смысл спорить с командой об оптимизации и кому-то что-то доказывать? Изучите свою целевую аудиторию, донесите информацию до команды, поставьте их перед фактом. И не нужны никакие споры и научные статьи доказывающие важность оптимизации.
Например:
Если бОльшая часть аудитории вашего проекта сидит на IE7, то у меня для вас плохие новости: вам в 2021 году нужно бует затачивать сайт под IE7. И поспорит ли тут с вами команда если они будут в курсе происходяго? — Нет.
Ещё пример:
Статистика показывает, что всемирный мобильный трафик превысил трафик с настольных ПК. Вёрстка под мобильные устройства очень важна? Принцип вёрстки "mobile first" приобретает всё больше смысла? — Мне по барабану. 80% целевой аудитории некоторых моих сайтов — это люди с большими мониторами и мобильными версиями этих сайтов я занимаюсь в последнюю очередь.
Так что не нужно ни с кем спорить и ничего доказывать. Просто делайте наилучшее решение для вашей аудитории и убедитесь, что разработчики понимают контекст происходящего.
Griboks
Поддерживаю. Я вот вообще links2 использую. Думаю, что это проблема браузеров, а не сайтов. Когда пустой хром потребляет 300 Мб, это просто смешно.
NetBUG
You must enable Javascript to visit this site.
QtRoS
Ну с Хромом мало софта в эти дни может конкурировать по сложности и функциональности. Сейчас уже что-то вроде миллиона страниц в стандарте веба. А ещё дополнительно к этому реализованы всякие плюшки вроде работы с локалями, эмоджи, всеми возможными форматами изображений (причем с постепенное загрузкой), да и много всего прочего, что я сразу не вспомнил. А ещё гугловые сервисы: авторизация, синхронизация между устройствами, etc. И все это хотя бы частично реализовано библиотеки, которые должны быть смаппированы в память. В этом свете пара сотен МБ не выглядит преступлением на мой взгляд.
Griboks
Разделите 300 Мб на 1000 страниц или даже 10 000 функций, описанных на страницах. Получаем 30 кб на каждую функцию. Разве функция запомнить логин/пароль от сайта стоит так много оперативки? Это же 16 страниц печатного текста а4, в блокноте записывать - и то меньше будет.
Metotron0
Коллеги сегодня озвучили node_modules в > 1 ГБ на одном из проектов.
Drag13 Автор
Справедливости ради, вряд-ли это 1+ ГБ сорцов. Вебпак, тайпскрипт, сайпрус/селениум/пюпитер и другие инструменты не используются соурс кодом напрямую, но могут лежать в node_modules. Но вообще это боль.
Max_JK
Про node_modules пора уже забыть, кроме огромного размера, сама их установка может занять приличное время, а если проект разросся то добавление лишней зависимости может занять 5-10 минут
как альтернативу можно использовать yarn 2й версии, всё хранится в одном месте в сжатом виде и подключается самим yarn напрямую в коде, работает на порядок быстрей, с нескольких минут время установки уменьшилось до пары секунд
причем в самом проекте для этого ничего менять не нужно
Vitaly83vvp
Вы забываете про картинки и видео. Их не только много, они, иногда, и «весят» немало. Пусть сам HTML и будет весить 5кб (в чём я сомневаюсь), но остальное будет очень большим. Плюс к media добавляются CSS — никто не хочет показывать свой сайт со стилями браузера — нужны свои стили. Как минимум bootstrap. Либо, свой «монстр». А ещё и JS библиотеки для удобной работы/разработки: Angular, React, Vue, jQuery и другие страшные слова. Всё вместе требует >1Мб трафика. И это с кэшем. Без кэша будет больше.
А всё почему? А потому что пользователям нравятся красивые картинки, кнопки и другие элементы.
Объёмы, которые указали вы, соответствуют, сайтам с описанием различных стандартов. Да, там просто HTML с минимумом стилей (либо, вообще без) и без JS (ну, в большинстве случаев).
Так что, оптимизация нужна и она нужна, в главной степени, при разработке библиотек, который используют разработчики сайтов. Но и этим разработчикам нужно делать оптимизацию используемых ресурсов.
Зачем NNN-ядерный процессор, MMM-Гб оперативной памяти, топовая видеокарта, если простой сайт в браузере может подвесить систему, сожрав все ресурсы? Просто потому, что разработчики поленились сделать оптимизацию.
Сейчас, создание сайтов сводится к сборке из готовых компонентов, просто добавляя свой контент. Не задумываясь о том, сколько ресурсов требуется для этих «кирпичиков». Да, так проще и быстрее, но результат может быть отвратительным. Пользователи просто не будут задерживаться на этих сайтах (ну, разве что, если комп зависнет).