Цифры говорят нам о том, что рост объёмов JavaScript-кода плохо влияет на производительность веб-проектов. Если так будет продолжаться и дальше, то уже очень скоро при загрузке средней страницы будет передаваться как минимум 400 Кб JS-кода. И это — всего лишь объём передаваемых данных. Как и другие текстовые ресурсы, JavaScript-код практически всегда передаётся в сжатом виде. Пожалуй, сжатие — это единственное, что обычно делается правильно при передаче кода с сервера на клиент.
К сожалению, в то время как уменьшение времени передачи неких ресурсов вносит серьёзный вклад в то, что мы называем «производительностью», сжатие никак не влияет на то, сколько времени у браузера уйдёт на разбор и обработку скрипта после того, как он будет полностью загружен.
Если сервер отправляет клиенту 400 Кб сжатого JS-кода, то реальное количество кода, которое браузеру нужно обработать после декомпрессии полученных данных, будет находиться в районе мегабайта. То, насколько хорошо различные устройства справляются с подобной работой, зависит от самих этих устройств. Об этом много всего написано, но с уверенностью можно утверждать лишь то, что время, которое требуется даже для разбора вполне обычного для своего времени объёма кода, сильно варьируется между разными устройствами.
Взгляните, например, на этот мой простенький проект. В ходе загрузки страницы на клиент передаётся около 23 Кб несжатого JS-кода. Chrome, работающий на MacBook Pro, который выпущен в середине 2017 года, этот довольно-таки небольшой объём кода обрабатывает примерно за 25 мс. На Android-смартфоне Nokia 2, однако, похожий показатель разрастается до 190 мс. Нельзя сказать, что это очень мало, но в любом случае страница становится интерактивной достаточно быстро.
Теперь — важный вопрос. Как вы думаете, как простой смартфон Nokia 2 справляется со средними современными страницами? На самом деле — просто ужасно. Просмотр веб-страниц, даже на быстром интернет-соединении, заставляет пользователя упражняться в терпеливости, так как работа с нагруженными JavaScript-кодом страницами становится возможной только после изрядного периода ожидания.
Обзор производительности Nokia 2 при просмотре страницы, содержащей большой объём JS-кода, обработка которого блокирует главный поток
Хотя и устройства, с помощью которых просматривают веб-страницы, и сети, по которым передаются данные, за последние годы серьёзно улучшились, исследования показывают, что все эти улучшения «съедаются» большими объёмами JS-кода, включаемого в состав страниц. Нам нужно ответственно использовать JavaScript. Ответственность начинается с понимания того, что именно мы создаём, и с того, как именно мы это делаем.
Сравнение идеологии «сайтов» и «приложений»
Странные вещи происходят с неточными терминами, которые мы используем для того, чтобы что-либо называть, хотя их значения, на интуитивном уровне, понятны всем. Иногда мы чрезмерно нагружаем смыслом слово «пчела», называя «пчёлами» и ос, даже хотя разница между пчёлами и осами весьма существенна. Учёт подобных различий может привести к тому, что с «пчёлами» и «осами» будут поступать по-разному. Например, мы собираемся уничтожить осиное гнездо, но если речь идёт о пчёлах, насекомых куда более полезных и уязвимых, их гнездо, расположенное в неудачном месте, вероятно, будет решено не уничтожить, а куда-нибудь перенести.
Подобная же свобода может наблюдаться и в том, как мы пользуемся терминами «веб-сайт» и «веб-приложение». Различия этих понятий гораздо менее очевидны, чем различия между настоящими осами и медоносными пчёлами, но если эти понятия объединить, подобное может привести к очень неприятным последствиям. Неприятности начинаются тогда, когда мы что-либо позволяем себе в зависимости от того, является ли некий проект «просто веб-сайтом» или «полномасштабным веб-приложением». Если вы создаёте информационный сайт для некоей компании, то, скорее всего, вы не будете полагаться на мощный фреймворк для управления изменениями DOM или для реализации маршрутизации на клиенте. Как минимум, я надеюсь, что это так. Использование инструментов, которые плохо подходят для решения неких задач, не только повредит тем, кто будет пользоваться сайтом, но и, вероятно, плохо скажется на процессе разработки.
При разработке веб-приложения всё выглядит иначе. Мы устанавливаем пакеты, что сопровождается добавлением в проект сотен, если не тысяч, зависимостей. При этом мы даже не уверены в безопасности некоторых из них. Мы пишем сложные конфигурации для бандлеров. При работе в подобной повсеместно встречающейся безумной среде разработки нужны знания и внимание для того, чтобы убедиться в том, что то, что было собрано, получилось быстрым, что проект будет работать там, где он должен работать. Если вы в этом сомневаетесь — выполните команду npm ls --prod в корневой директории своего проекта и посмотрите, сможете ли вы назвать цель использования всего того, что выведет эта команда. Если даже вы сможете это сделать, это не распространяется на скрипты сторонних разработчиков. Уверен, несколько таких скриптов используется и в вашем проекте.
Мы забываем о том, что и веб-сайты, и веб-приложения занимают одну и ту же «экологическую нишу». И те и другие находятся под одинаковым влиянием среды, которая состоит из разнообразных сетей и устройств. Подобные ограничения никуда не исчезнут в том случае, если мы решим называть то, что мы разрабатываем, «приложением», да и устройства наших пользователей не станут волшебным образом гораздо быстрее, если мы назовём «сайт» «приложением».
В сферу нашей ответственности входит выяснение того, кто пользуется тем, что мы создаём, мы должны учитывать то, что условия, в которых разные пользователи подключаются к интернету, могут отличаться от тех, на которые мы рассчитываем. Мы, создавая что-либо, должны знать цель, ради достижения которой мы это создаём, а уже после этого заниматься разработкой того, что помогает достичь этой цели — даже в том случае, если разработка окажется не таким уж восхитительным занятием.
Это означает необходимость переоценки нашей зависимости от JavaScript, и того, как его использование, в частности, в ущерб HTML и CSS, может привести нас к применению нерациональных паттернов, вредящих производительности и доступности веб-проектов.
Не позволяйте фреймворкам навязывать вам нерациональные паттерны
Я был свидетелем обнаружения странных вещей в кодовых базах, когда работал с командами, зависящими от фреймворков, для того, чтобы помочь им быть более продуктивными. У множества подобных находок есть одна общая черта, которая заключается в том, что то, как они написаны, часто приводит к проблемам с доступностью и производительностью сайтов. Например, рассмотрим следующий React-компонент:
import React, { Component } from "react";
import { validateEmail } from "helpers/validation";
class SignupForm extends Component {
constructor (props) {
this.handleSubmit = this.handleSubmit.bind(this);
this.updateEmail = this.updateEmail.bind(this);
this.state.email = "";
}
updateEmail (event) {
this.setState({
email: event.target.value
});
}
handleSubmit () {
// если адрес почты проходит проверку - отправить данные
if (validateEmail(this.state.email)) {
// ...
}
}
render () {
return (
<div>
<span class="email-label">Enter your email:</span>
<input type="text" id="email" onChange={this.updateEmail} />
<button onClick={this.handleSubmit}>Sign Up</button>
</div>
);
}
}
Тут можно найти несколько заметных проблем, касающихся доступности проекта:
- Форма, которая не использует элемент
<form>
, это уже не форма. На самом деле, исправить это можно, просто указав role=«form» у родительского элемента<div>
, но если вы создаёте форму, а то, что мы видим, определённо, выглядит как форма, используйте элемент<form>
, настроив соответствующим образом атрибутыaction
иmethod
. Атрибутaction
играет тут важнейшую роль, так как он позволяет форме сделать хоть что-то даже в том случае, если JavaScript оказывается недоступным (естественно, если компонент был отрендерен на сервере). - Тег
<span>
не является заменителем для тега<label>
, который даёт некоторые возможности, касающиеся доступности проектов, которых нет у<span>
. - Элемент
<button>
без атрибутаtype="submit"
— это просто кнопка, нажатие на которую приводит к вызову привязанного к ней обработчика события. Если мы хотим что-то сделать с данными до отправки формы, кнопке нужно назначить атрибутtype="submit"
, и переместить код из обработчика событияonClick
в обработчик события формыonSubmit
. - Между прочим, зачем использовать JavaScript для проверки адреса электронной почты, в то время как HTML5 даёт в наше распоряжение элементы управления, поддерживающие проверку введённых данных практически во всех браузерах — вплоть до IE10? Тут мы видим упущенную возможность воспользоваться функционалом, который уже есть в браузере и применить подходящий тип элемента, а также атрибут required. Правда, применяя подобные конструкции, учитывайте то, что наладка их нормального взаимодействия с программами для чтения экрана потребует некоторых усилий.
Учтя вышесказанное, подвергнем код компонента рефакторингу:
import React, { Component } from "react";
class SignupForm extends Component {
constructor (props) {
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit (event) {
// Нужно в том случае, если мы отправляем данные на сервер с использованием XHR
// (но будет работать и в том случае, если страница рендерится на сервере, а в браузере отключён JS).
event.preventDefault();
// Продолжаем работу…
}
render () {
return (
<form method="POST" action="/signup" onSubmit={this.handleSubmit}>
<label for="email" class="email-label">Enter your email:</label>
<input type="email" id="email" required />
<button type="submit">Sign Up</button>
</form>
);
}
}
Теперь то, что рендерит этот компонент, не только оказывается более доступным, но и для реализации того же функционала, что и раньше, используется меньше JS-кода. В мире, который буквально утонул в JavaScript, избавление от нескольких строк кода должно восприниматься как нечто позитивное. Браузер даёт нам массу возможностей, и нам нужно стремиться к тому, чтобы пользоваться этими возможностями как можно чаще.
Я не хочу тут сказать, что проблемы с доступностью страниц возникают исключительно при использовании неких фреймворков. Я имею в виду то, что, чрезмерно полагаясь на JavaScript, разработчик, в итоге, будет попросту упускать много важных возможностей HTML и CSS. Эти пробелы в знаниях часто ведут к ошибкам, причём, мы об этих ошибках даже и не подозреваем. Фреймворки могут быть полезными инструментами, увеличивающими производительность разработчика, но постоянное изучение возможностей базовых веб-технологий крайне важно в создании удобных, пригодных к использованию продуктов, вне зависимости от применяемых при их разработке вспомогательных инструментов.
Положитесь на возможности веб-платформы и обеспечьте своим проектам светлое будущее
Раз уж мы говорим о фреймворках, нельзя не отметить то, что веб-платформа, сама по себе, это тоже огромный фреймворк. Как было показано в предыдущем разделе, мы оказываемся в более выгодном положении в том случае, если можем рассчитывать на устоявшиеся паттерны работы с разметкой и на возможности браузера. Альтернатива этим стандартным возможностям заключается в том, чтобы снова их изобрести. Не стоит и говорить о том, что подобное «изобретательство» сопряжено с немалыми трудностями. А что если бы подобные проблемы, каждый по-своему, решали бы авторы всех устанавливаемых нами JavaScript-пакетов?
▍Одностраничные приложения
Одна из слабостей разработчиков, которую они легко себе позволяют, заключается в применении модели одностраничных приложений (Single Page Application, SPA) даже в тех проектах, для которых эта модель не подходит. Конечно, такие проекты выигрывают от того, что они воспринимаются пользователями как более производительные за счёт маршрутизации, выполняемой средствами клиента. Но в чём минусы применения модели SPA? Встроенные возможности браузера по навигации по страницам, хотя и построены по синхронной модели, дают проекту массу преимуществ. Одно из них заключается в том, что управление историей посещений осуществляется благодаря реализации сложной спецификации. Пользователи без JavaScript, отключили ли они его сами или нет, не потеряют возможность работать с проектом. Для того чтобы одностраничное приложение было бы доступным в браузерах с отключённым JavaScript, внезапно оказывается, что нужно уделять немалое внимание серверному рендерингу.
Сравнение разных вариантов загрузки экспериментального приложения на медленном канале связи. Рендеринг приложения слева полностью зависит от JavaScript. Приложение справа рендерится на сервере, но затем использует, на клиенте, метод hydrate() для подключения компонентов к уже созданной на сервере разметке
Здесь можно видеть то, что приложение, которое рендерится на клиенте, несколько секунд демонстрирует пользователю пустой экран, после чего выводит готовый интерфейс.
Приложение, которое рендерится на сервере и приводится в рабочее состояние на клиенте, довольно быстро выводит основные элементы интерфейса, но пользоваться им можно примерно через то же время, что и приложением, которое рендерится целиком на клиенте.
Доступность приложения так же страдает и в том случае, если маршрутизатор, расположенный на клиенте, не может сообщить пользователю о том, что изменилось на просматриваемой им странице. Это может заставить пользователя полагаться на вспомогательные технологии для того, чтобы разузнать о том, что именно изменилось на странице, в результате работа пользователя с сайтом значительно усложняется.
Далее, тут же можно встретить нашего старого врага — излишнюю нагрузку на систему. Некоторые клиентские маршрутизаторы отличаются очень маленькими размерами. Но если делать проект на React, воспользоваться совместимым маршрутизатором, и, возможно, библиотекой для управления состоянием приложения, это значит, что придётся принять то, что в нём будет присутствовать некий объём служебного кода, от которого никуда уже не деться. А именно, в данном случае это примерно 135 Кб такого кода. Тщательно анализируйте проекты, которые создаёте, и то, стоит ли клиентская маршрутизация создаваемой ей дополнительной нагрузки на систему. Обычно бывает так, что лучше от системы клиентской маршрутизации отказаться.
Если вас беспокоят ощущения пользователя, если вы хотите, чтобы сайт казался бы ему быстрым, то вы можете положиться на атрибут ссылок rel=prefetch, который позволяет организовать заблаговременную загрузку документов из одного и того же источника. Использование этого атрибута оказывает огромное влияние на улучшение производительности проекта, воспринимаемой пользователями, так как страницы, ссылки на которые оформлены с использованием этого атрибута, при щелчках по этим ссылкам, мгновенно загружаются из кэша. Кроме того, так как предварительная загрузка данных имеет низкий приоритет, она вряд ли будет конкурировать за полосу пропускания с важными ресурсами.
HTML-код, на который ведёт ссылка writing/, предварительно загружен при посещении главной страницы сайта. Когда пользователь щёлкает по соответствующей ссылке, HTML-код мгновенно загружается из кэша браузера
Главная проблема, которая может возникнуть с предварительной загрузкой страниц, и о которой нужно знать, заключается в том, что такая загрузка может оказаться пустой тратой времени и ресурсов. Для её решения можно, например, воспользоваться небольшим скриптом Quicklink от Google, который смягчает эту проблему. Он проверяет, пользуется ли текущий клиент медленным соединением, включён ли у него режим экономии данных, и, по умолчанию, позволяет избегать предварительной загрузки материалов из источников, отличающихся от источника страницы.
Для того чтобы сайт выглядел бы в глазах пользователей, которые многократно посещают его, быстрым, можно воспользоваться сервис-воркерами. Их можно применять независимо от того, используется ли в проекте клиентская система маршрутизации или нет, учитывая то, что вы знакомы с некоторыми особенностями сервис-воркеров. Выполняя кэширование маршрутов средствами сервис-воркеров, мы получаем множество тех же преимуществ, которые характерны для заблаговременной загрузки материалов некоторых ссылок, но в нашем распоряжении оказывается гораздо более обширные возможности по работе с запросами и ответами. Воспринимаете ли вы свой сайт как «приложение» или нет, оснащение его сервис-воркером, возможно, пример одного из самых ответственных вариантов использования JavaScript из существующих в наше время.
▍JavaScript не предназначен для формирования макетов
Если мы устанавливаем JS-пакет, предназначенный для решения задач, касающихся макетов страниц, то нам самое время проявить повышенную осторожность и задаться вопросом о том, чего мы пытаемся с помощью этого пакета добиться. CSS создан специально для построения макетов страниц, для того, чтобы эффективно его использовать, не нужно никаких абстракций. Большинство задач построения макетов, которые пытаются решать средствами JavaScript, наподобие размещения элементов, их выравнивания, настройки их размеров, наподобие управления текстом или даже полного формирования макетов средствами JavaScript, в наши дни может быть решено с помощью CSS. Современные средства создания макетов наподобие Flexbox и Grid, достаточно хорошо поддерживаются браузерами, поэтому у нас нет нужды разрабатывать проекты, основанные на фреймворках для работы с макетами. Кстати, CSS — это ведь тоже фреймворк. Когда в нашем распоряжении оказывается такая возможность, как запросы свойств, прогрессивное улучшение макетов для поддержки новых средств их формирования, как выясняется, оказывается не такой уж и сложной задачей.
/* Тут будут стили, рассчитанные, в первую очередь, на мобильные устройства и не пользующиеся возможностями CSS Grid. */
/* Правило @supports игнорируется браузерами, которые не поддерживают технологию CSS Grid, или не поддерживают это правило. */
@supports (display: grid) {
/* Стили для более крупных экранов */
@media (min-width: 40em) {
/* Сюда попадают прогрессивно улучшаемые стили для CSS Grid */
}
}
Использование возможностей JavaScript для решения задач формирования макетов страниц и настройки их внешнего вида — это не новость. Это то, чем мы занимались в 2009 году, когда жили в атмосфере самообмана, говоря о том, что каждый сайт должен выглядеть в IE6 так же, как и в более продвинутых браузерах того времени. Если мы и сегодня, в 2019 году, продолжаем разрабатывать сайты так, чтобы они одинаково выглядели во всех браузерах, это значит, что нам нужно пересмотреть наши цели. Всегда будут некоторые браузеры, которые нужно поддерживать, и которые не обладают теми же возможностями, что и самые современные браузеры. Полное внешнее сходство проектов на всех платформах — это не только напрасная трата сил, это ещё и принципиальный враг идеи прогрессивных улучшений.
Итоги: я не собираюсь стать убийцей JavaScript
Поймите меня правильно, я не отношусь к врагам JavaScript. Благодаря этому языку я построил карьеру, и, если честно, JavaScript, уже более десяти лет, приносит мне массу удовольствия. Как бывает в любых длительных взаимоотношениях, чем больше времени я провожу, работая с JavaScript, тем лучше я его узнаю. Это — зрелый язык, обладающий множеством возможностей, который, с каждым годом, становится всё лучше.
Однако иногда мне кажется, что в наших с JavaScript отношениях что-то разладилось. Я его критикую. Или, если точнее, я критикую существующую тенденцию считать JavaScript главным инструментом сайтостроения, к которому прибегают в первую очередь, не глядя ни на что другое. Когда я проанализировал очередной бандл, похожий на запутанную новогоднюю гирлянду, мне стало ясно, что веб опьянён JavaScript. Мы прибегаем к этому языку практически по любому поводу, даже в случаях, когда обстоятельства этого и не требуют. Иногда я размышляю о том, насколько тяжёлыми могут быть последствия подобного отношения к JS.
Я планирую продолжать писать о JavaScript и о веб-разработке, продолжать искать способы рационального использования веб-технологий. Надеюсь, вместе мы сделаем современный веб лучше.
Уважаемые читатели! Как вы думаете, действительно ли современный веб перегружен JavaScript-кодом?
Автор: ru_vds