Избегайте ловушек автоматического встраивания кода

Опубликовано: 2022-03-10
Краткий обзор ↬ Чрезмерное использование встроенного кода CSS или JS, в отличие от предоставления кода через статические ресурсы, может снизить производительность сайта. В этой статье мы узнаем, как вместо этого загружать динамический код через статические файлы, избегая недостатков слишком большого количества встроенного кода.

Встраивание — это процесс включения содержимого файлов непосредственно в документ HTML: файлы CSS могут быть встроены в элемент style , а файлы JavaScript могут быть встроены в элемент script :

 <style> /* CSS contents here */ </style> <script> /* JS contents here */ </script>

Распечатывая код уже в выводе HTML, встраивание позволяет избежать запросов, блокирующих рендеринг, и выполняет код до рендеринга страницы. Таким образом, это полезно для улучшения воспринимаемой производительности сайта (т. е. времени, необходимого для того, чтобы страница стала пригодной для использования). Например, мы можем использовать буфер данных, доставляемых сразу при загрузке сайта (около 14 КБ), для встроенного критически важные стили, включая стили содержимого верхней части страницы (как это было сделано на предыдущем сайте Smashing Magazine), а также размеры шрифта, ширину и высоту макета, чтобы избежать повторного рендеринга макета при доставке остальных данных. .

Однако чрезмерное встраивание кода также может отрицательно сказаться на производительности сайта: поскольку код не кэшируется, один и тот же контент отправляется клиенту повторно, и его нельзя предварительно кэшировать с помощью сервис-воркеров или кэшируются и доступны из сети доставки контента. Кроме того, встроенные сценарии считаются небезопасными при реализации политики безопасности содержимого (CSP). Затем разумной стратегией является встраивание тех критических частей CSS и JS, которые ускоряют загрузку сайта, но в противном случае их избегают, насколько это возможно.

Чтобы избежать встраивания, в этой статье мы рассмотрим, как преобразовать встроенный код в статические ресурсы: вместо того, чтобы печатать код в выводе HTML, мы сохраняем его на диск (фактически создавая статический файл) и добавляем соответствующий <script> или <link> , чтобы загрузить файл.

Давайте начнем!

Рекомендуемая литература : Безопасность WordPress как процесс

Еще после прыжка! Продолжить чтение ниже ↓

Когда следует избегать встраивания

Не существует волшебного рецепта, чтобы определить, должен ли какой-то код быть встроен или нет, однако может быть довольно очевидно, когда какой-то код не должен быть встроен: когда он включает большой кусок кода и когда он не нужен немедленно.

Например, сайты WordPress встраивают шаблоны JavaScript для отображения Медиа-менеджера (доступного на странице Медиа-библиотеки в /wp-admin/upload.php ), печатая значительный объем кода:

Скриншот исходного кода страницы медиатеки
Шаблоны JavaScript, встроенные в WordPress Media Manager.

Занимая целых 43 КБ, размер этого фрагмента кода не является незначительным, и, поскольку он находится внизу страницы, он не нужен сразу. Следовательно, было бы разумно обслуживать этот код через статические ресурсы или печатать его внутри вывода HTML.

Далее давайте посмотрим, как преобразовать встроенный код в статические ресурсы.

Запуск создания статических файлов

Если содержимое (те, которые должны быть встроены) берутся из статического файла, то делать нечего, кроме как просто запросить этот статический файл вместо встраивания кода.

Однако для динамического кода мы должны планировать, как и когда создавать статический файл с его содержимым. Например, если сайт предлагает параметры конфигурации (такие как изменение цветовой схемы или фонового изображения), когда должен быть создан файл, содержащий новые значения? У нас есть следующие возможности для создания статических файлов из динамического кода:

  1. По требованию
    Когда пользователь получает доступ к содержимому в первый раз.
  2. По изменению
    Когда источник динамического кода (например, значение конфигурации) изменился.

Сначала рассмотрим по запросу. Когда пользователь впервые заходит на сайт, скажем, через /index.html , статический файл (например header-colors.css ) еще не существует, поэтому его необходимо сгенерировать. Последовательность событий следующая:

  1. Пользователь запрашивает /index.html ;
  2. При обработке запроса сервер проверяет, существует ли файл header-colors.css . Поскольку это не так, он получает исходный код и создает файл на диске;
  3. Он возвращает ответ клиенту, включая тег <link rel="stylesheet" type="text/css" href="/staticfiles/header-colors.css">
  4. Браузер извлекает все ресурсы, включенные в страницу, включая header-colors.css ;
  5. К тому времени этот файл существует, поэтому он обслуживается.

Однако последовательность событий также может быть иной, что приведет к неудовлетворительному исходу. Например:

  1. Пользователь запрашивает /index.html ;
  2. Этот файл уже закэширован браузером (или каким-то другим прокси, или через Service Workers), поэтому запрос никогда не отправляется на сервер;
  3. Браузер извлекает все ресурсы, включенные в страницу, включая header-colors.css . Однако это изображение не кэшируется в браузере, поэтому запрос отправляется на сервер;
  4. Сервер еще не сгенерировал header-colors.css (например, он был только что перезапущен);
  5. Он вернет 404.

В качестве альтернативы мы могли бы сгенерировать header-colors.css не при запросе /index.html , а при запросе самого /header-colors.css . Однако, поскольку этот файл изначально не существует, запрос уже обрабатывается как 404. Несмотря на то, что мы могли обойти его, изменив заголовки, чтобы изменить код состояния на 200 и вернуть содержимое изображения, это ужасный способ ведения дел, поэтому мы не будем рассматривать эту возможность (мы намного лучше этого!)

Остается только один вариант: создание статического файла после изменения его источника.

Создание статического файла при изменении источника

Обратите внимание, что мы можем создавать динамический код как из источников, зависящих от пользователя, так и из источников, зависящих от сайта. Например, если тема позволяет изменить фоновое изображение сайта и этот параметр настроен администратором сайта, то статический файл может быть создан как часть процесса развертывания. С другой стороны, если сайт позволяет пользователям изменять фоновое изображение для своих профилей, статический файл должен создаваться во время выполнения.

Короче говоря, у нас есть эти два случая:

  1. Конфигурация пользователя
    Процесс должен запускаться, когда пользователь обновляет конфигурацию.
  2. Конфигурация сайта
    Процесс должен запускаться, когда администратор обновляет конфигурацию сайта или перед развертыванием сайта.

Если бы мы рассмотрели два случая независимо друг от друга, то для № 2 мы могли бы спроектировать процесс на любом технологическом стеке, какой захотим. Однако мы хотим реализовать не два разных решения, а уникальное решение, которое может справиться с обоими случаями. А поскольку, начиная с пункта 1, процесс создания статического файла должен запускаться на работающем сайте, необходимо разработать этот процесс на основе того же стека технологий, на котором работает сайт.

При разработке процесса наш код должен будет обрабатывать конкретные обстоятельства как № 1, так и № 2:

  • Версии
    Статический файл должен быть доступен с параметром «версия», чтобы сделать предыдущий файл недействительным при создании нового статического файла. В то время как № 2 может просто иметь ту же версию, что и сайт, № 1 необходимо использовать динамическую версию для каждого пользователя, возможно, сохраненную в базе данных.
  • Расположение сгенерированного файла
    #2 создает уникальный статический файл для всего сайта (например, /staticfiles/header-colors.css ), а #1 создает статический файл для каждого пользователя (например /staticfiles/users/leo/header-colors.css ).
  • Инициирующее событие
    В то время как для № 1 статический файл должен выполняться во время выполнения, для № 2 он также может выполняться как часть процесса сборки в нашей промежуточной среде.
  • Развертывание и распространение
    Статические файлы в # 2 могут быть легко интегрированы в пакет развертывания сайта, не создавая проблем; статические файлы в № 1, однако, не могут, поэтому процесс должен обрабатывать дополнительные проблемы, такие как несколько серверов за балансировщиком нагрузки (будут ли статические файлы создаваться только на 1 сервере или на всех из них, и как?).

Давайте спроектируем и реализуем процесс дальше. Для каждого создаваемого статического файла мы должны создать объект, содержащий метаданные файла, вычислить его содержимое из динамических источников и, наконец, сохранить статический файл на диск. В качестве примера использования пояснений ниже мы создадим следующие статические файлы:

  1. header-colors.css с некоторым стилем из значений, сохраненных в базе данных
  2. welcomeuser-data.js , содержащий объект JSON с данными пользователя под некоторой переменной: window.welcomeUserData = {name: "Leo"}; .

Ниже я опишу процесс создания статических файлов для WordPress, для чего мы должны основывать стек на функциях PHP и WordPress. Функцию создания статических файлов перед развертыванием можно запустить, загрузив специальную страницу, выполняющую шорткод [create_static_files] , как я описал в предыдущей статье.

Дополнительная рекомендуемая литература : Создание сервисного работника: пример из практики

Представление файла как объекта

Мы должны смоделировать файл как объект PHP со всеми соответствующими свойствами, поэтому мы можем сохранить файл на диске в определенном месте (например, в /staticfiles/ или /staticfiles/users/leo/ ) и знать, как запросить файл соответственно. Для этого мы создаем Resource интерфейса, возвращающий как метаданные файла (имя файла, директорию, тип: «css» или «js», версию и зависимости от других ресурсов), так и его содержимое.

 interface Resource { function get_filename(); function get_dir(); function get_type(); function get_version(); function get_dependencies(); function get_content(); }

Чтобы сделать код пригодным для сопровождения и повторного использования, мы следуем принципам SOLID, для которых мы устанавливаем схему наследования объектов для ресурсов, чтобы постепенно добавлять свойства, начиная с абстрактного класса ResourceBase , от которого будут наследоваться все наши реализации Resource:

 abstract class ResourceBase implements Resource { function get_dependencies() { // By default, a file has no dependencies return array(); } }

Следуя SOLID, мы создаем подклассы всякий раз, когда свойства различаются. Как указывалось ранее, расположение сгенерированного статического файла и версия для его запроса будут различаться в зависимости от файла, относящегося к пользователю или конфигурации сайта:

 abstract class UserResourceBase extends ResourceBase { function get_dir() { // A different file and folder for each user $user = wp_get_current_user(); return "/staticfiles/users/{$user->user_login}/"; } function get_version() { // Save the resource version for the user under her meta data. // When the file is regenerated, must execute `update_user_meta` to increase the version number $user_id = get_current_user_id(); $meta_key = "resource_version_".$this->get_filename(); return get_user_meta($user_id, $meta_key, true); } } abstract class SiteResourceBase extends ResourceBase { function get_dir() { // All files are placed in the same folder return "/staticfiles/"; } function get_version() { // Same versioning as the site, assumed defined under a constant return SITE_VERSION; } }

Наконец, на последнем уровне мы реализуем объекты для файлов, которые хотим сгенерировать, добавляя имя файла, тип файла и динамический код через функцию get_content :

 class HeaderColorsSiteResource extends SiteResourceBase { function get_filename() { return "header-colors"; } function get_type() { return "css"; } function get_content() { return sprintf( " .site-title a { color: #%s; } ", esc_attr(get_header_textcolor()) ); } } class WelcomeUserDataUserResource extends UserResourceBase { function get_filename() { return "welcomeuser-data"; } function get_type() { return "js"; } function get_content() { $user = wp_get_current_user(); return sprintf( "window.welcomeUserData = %s;", json_encode( array( "name" => $user->display_name ) ) ); } }

При этом мы смоделировали файл как объект PHP. Далее нам нужно сохранить его на диск.

Сохранение статического файла на диск

Сохранение файла на диск может быть легко выполнено с помощью собственных функций, предоставляемых языком. В случае PHP это достигается с помощью функции fwrite . Кроме того, мы создаем служебный класс ResourceUtils с функциями, предоставляющими абсолютный путь к файлу на диске, а также его путь относительно корня сайта:

 class ResourceUtils { protected static function get_file_relative_path($fileObject) { return $fileObject->get_dir().$fileObject->get_filename().".".$fileObject->get_type(); } static function get_file_path($fileObject) { // Notice that we must add constant WP_CONTENT_DIR to make the path absolute when saving the file return WP_CONTENT_DIR.self::get_file_relative_path($fileObject); } } class ResourceGenerator { static function save($fileObject) { $file_path = ResourceUtils::get_file_path($fileObject); $handle = fopen($file_path, "wb"); $numbytes = fwrite($handle, $fileObject->get_content()); fclose($handle); } }

Затем всякий раз, когда исходный код изменяется и статический файл необходимо регенерировать, мы выполняем ResourceGenerator::save , передавая объект, представляющий файл, в качестве параметра. Код ниже регенерирует и сохраняет на диске файлы «header-colors.css» и «welcomeuser-data.js»:

 // When need to regenerate header-colors.css, execute: ResourceGenerator::save(new HeaderColorsSiteResource()); // When need to regenerate welcomeuser-data.js, execute: ResourceGenerator::save(new WelcomeUserDataUserResource());

Как только они существуют, мы можем поставить файлы в очередь для загрузки с помощью тегов <script> и <link> .

Постановка в очередь статических файлов

Постановка в очередь статических файлов ничем не отличается от постановки в очередь любого ресурса в WordPress: через функции wp_enqueue_script и wp_enqueue_style . Затем мы просто перебираем все экземпляры объекта и используем тот или иной хук в зависимости от их get_type() , равного "js" или "css" .

Сначала мы добавляем служебные функции, чтобы предоставить URL-адрес файла и указать тип JS или CSS:

 class ResourceUtils { // Continued from above... static function get_file_url($fileObject) { // Add the site URL before the file path return get_site_url().self::get_file_relative_path($fileObject); } static function is_css($fileObject) { return $fileObject->get_type() == "css"; } static function is_js($fileObject) { return $fileObject->get_type() == "js"; } }

Экземпляр класса ResourceEnqueuer будет содержать все файлы, которые необходимо загрузить; при вызове его функции enqueue_scripts и enqueue_styles будут ставить в очередь, выполняя соответствующие функции WordPress ( wp_enqueue_script и wp_enqueue_style соответственно):

 class ResourceEnqueuer { protected $fileObjects; function __construct($fileObjects) { $this->fileObjects = $fileObjects; } protected function get_file_properties($fileObject) { $handle = $fileObject->get_filename(); $url = ResourceUtils::get_file_url($fileObject); $dependencies = $fileObject->get_dependencies(); $version = $fileObject->get_version(); return array($handle, $url, $dependencies, $version); } function enqueue_scripts() { $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $this->fileObjects); foreach ($jsFileObjects as $fileObject) { list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject); wp_register_script($handle, $url, $dependencies, $version); wp_enqueue_script($handle); } } function enqueue_styles() { $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $this->fileObjects); foreach ($cssFileObjects as $fileObject) { list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject); wp_register_style($handle, $url, $dependencies, $version); wp_enqueue_style($handle); } } }

Наконец, мы создаем объект класса ResourceEnqueuer со списком объектов PHP, представляющих каждый файл, и добавляем хук WordPress для выполнения постановки в очередь:

 // Initialize with the corresponding object instances for each file to enqueue $fileEnqueuer = new ResourceEnqueuer( array( new HeaderColorsSiteResource(), new WelcomeUserDataUserResource() ) ); // Add the WordPress hooks to enqueue the resources add_action('wp_enqueue_scripts', array($fileEnqueuer, 'enqueue_scripts')); add_action('wp_print_styles', array($fileEnqueuer, 'enqueue_styles'));

Вот и все: Статические файлы, поставленные в очередь, будут запрашиваться при загрузке сайта в клиенте. Нам удалось избежать вывода встроенного кода и загрузки статических ресурсов.

Далее мы можем применить несколько улучшений для дополнительного прироста производительности.

Рекомендуемая литература : Введение в автоматическое тестирование плагинов WordPress с помощью PHPUnit.

Объединение файлов вместе

Несмотря на то, что HTTP/2 уменьшил потребность в объединении файлов, он по-прежнему делает сайт быстрее, потому что сжатие файлов (например, с помощью GZip) будет более эффективным, а браузеры (такие как Chrome) имеют большие накладные расходы на обработку многих ресурсов. .

К настоящему моменту мы смоделировали файл как объект PHP, что позволяет нам рассматривать этот объект как входные данные для других процессов. В частности, мы можем повторить описанный выше процесс, чтобы объединить все файлы одного типа вместе и использовать объединенную версию вместо всех независимых файлов. Для этого мы создаем функцию get_content , которая просто извлекает содержимое из каждого ресурса в $fileObjects и снова печатает его, производя агрегацию всего содержимого из всех ресурсов:

 abstract class SiteBundleBase extends SiteResourceBase { protected $fileObjects; function __construct($fileObjects) { $this->fileObjects = $fileObjects; } function get_content() { $content = ""; foreach ($this->fileObjects as $fileObject) { $content .= $fileObject->get_content().PHP_EOL; } return $content; } }

Мы можем объединить все файлы в файл bundled-styles.css , создав класс для этого файла:

 class StylesSiteBundle extends SiteBundleBase { function get_filename() { return "bundled-styles"; } function get_type() { return "css"; } }

Наконец, мы просто ставим в очередь эти связанные файлы, как и раньше, вместо всех независимых ресурсов. Для CSS мы создаем пакет, содержащий файлы header-colors.css , background-image.css и font-sizes.css , для которых мы просто создаем экземпляр StylesSiteBundle с объектом PHP для каждого из этих файлов (аналогично мы можем создать JS-файл). пакетный файл):

 $fileObjects = array( // CSS new HeaderColorsSiteResource(), new BackgroundImageSiteResource(), new FontSizesSiteResource(), // JS new WelcomeUserDataUserResource(), new UserShoppingItemsUserResource() ); $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $fileObjects); $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $fileObjects); // Use this definition of $fileEnqueuer instead of the previous one $fileEnqueuer = new ResourceEnqueuer( array( new StylesSiteBundle($cssFileObjects), new ScriptsSiteBundle($jsFileObjects) ) );

Вот и все. Теперь мы будем запрашивать только один файл JS и один файл CSS вместо многих.

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

Атрибуты async / defer для ресурсов JS

Мы можем добавить атрибуты async и defer к тегу <script> , чтобы изменить время загрузки, анализа и выполнения файла JavaScript, чтобы расставить приоритеты для критического JavaScript и отодвинуть все некритическое как можно позже, тем самым снизив очевидную загрузку сайта. время.

Чтобы реализовать эту функцию, следуя принципам SOLID, мы должны создать новый интерфейс JSResource (который наследуется от Resource ), содержащий функции is_async и is_defer . Однако это закроет дверь для тегов <style> , которые в конечном итоге тоже будут поддерживать эти атрибуты. Таким образом, имея в виду адаптивность, мы используем более открытый подход: мы просто добавляем общий метод get_attributes к интерфейсу Resource , чтобы сохранить его гибкость для добавления к любому атрибуту (либо уже существующему, либо еще не изобретенному) для обоих <script> теги <script> и <link> :

 interface Resource { // Continued from above... function get_attributes(); } abstract class ResourceBase implements Resource { // Continued from above... function get_attributes() { // By default, no extra attributes return ''; } }

WordPress не предлагает простого способа добавления дополнительных атрибутов к поставленным в очередь ресурсам, поэтому мы делаем это довольно хакерским способом, добавляя хук, который заменяет строку внутри тега через функцию add_script_tag_attributes :

 class ResourceEnqueuerUtils { protected static tag_attributes = array(); static function add_tag_attributes($handle, $attributes) { self::tag_attributes[$handle] = $attributes; } static function add_script_tag_attributes($tag, $handle, $src) { if ($attributes = self::tag_attributes[$handle]) { $tag = str_replace( " src='${src}'>", " src='${src}' ".$attributes.">", $tag ); } return $tag; } } // Initize by connecting to the WordPress hook add_filter( 'script_loader_tag', array(ResourceEnqueuerUtils::class, 'add_script_tag_attributes'), PHP_INT_MAX, 3 );

Мы добавляем атрибуты для ресурса при создании соответствующего экземпляра объекта:

 abstract class ResourceBase implements Resource { // Continued from above... function __construct() { ResourceEnqueuerUtils::add_tag_attributes($this->get_filename(), $this->get_attributes()); } }

Наконец, если ресурс welcomeuser-data.js не нужно запускать немедленно, мы можем установить его как defer :

 class WelcomeUserDataUserResource extends UserResourceBase { // Continued from above... function get_attributes() { return "defer='defer'"; } }

Поскольку он загружается как отложенный, сценарий загружается позже, перенося момент времени, когда пользователь может взаимодействовать с сайтом. Что касается прироста производительности, у нас все готово!

Осталось решить один вопрос, прежде чем мы сможем расслабиться: что происходит, когда сайт размещен на нескольких серверах?

Работа с несколькими серверами за балансировщиком нагрузки

Если наш сайт размещен на нескольких сайтах за балансировщиком нагрузки, и файл, зависящий от конфигурации пользователя, регенерируется, сервер, обрабатывающий запрос, должен каким-то образом загрузить регенерированный статический файл на все остальные серверы; в противном случае с этого момента другие серверы будут обслуживать устаревшую версию этого файла. как нам это сделать? Связь серверов друг с другом не только сложна, но и может в конечном итоге оказаться невыполнимой: что произойдет, если сайт будет работать на сотнях серверов из разных регионов? Понятно, что это не вариант.

Решение, которое я придумал, состоит в том, чтобы добавить уровень косвенности: вместо того, чтобы запрашивать статические файлы с URL-адреса сайта, они запрашиваются из местоположения в облаке, например из корзины AWS S3. Затем, после регенерации файла, сервер немедленно загрузит новый файл на S3 и будет обслуживать его оттуда. Реализация этого решения описана в моей предыдущей статье «Обмен данными между несколькими серверами через AWS S3».

Заключение

В этой статье мы рассмотрели, что встраивание кода JS и CSS не всегда идеально, потому что код должен многократно отправляться клиенту, что может сказаться на производительности, если объем кода значителен. В качестве примера мы видели, как WordPress загружает 43 КБ сценариев для печати Media Manager, которые представляют собой чистые шаблоны JavaScript и могут быть загружены как статические ресурсы.

Поэтому мы разработали способ сделать веб-сайт быстрее, преобразовав динамический встроенный код JS и CSS в статические ресурсы, что может улучшить кеширование на нескольких уровнях (в клиенте, сервис-воркерах, CDN), позволяет дополнительно объединять все файлы вместе. только в один ресурс JS/CSS, чтобы улучшить соотношение при сжатии вывода (например, через GZip) и избежать накладных расходов в браузерах на одновременную обработку нескольких ресурсов (например, в Chrome), а также позволяет добавлять атрибуты async или defer к тегу <script> , чтобы ускорить взаимодействие с пользователем, тем самым сократив видимое время загрузки сайта.

В качестве полезного побочного эффекта разделение кода на статические ресурсы также позволяет сделать код более разборчивым, имея дело с единицами кода, а не с большими фрагментами HTML, что может привести к лучшему сопровождению проекта.

Разработанное нами решение было выполнено на PHP и включает в себя несколько фрагментов кода для WordPress, однако сам код чрезвычайно прост, всего несколько интерфейсов, определяющих свойства и объекты, реализующие эти свойства в соответствии с принципами SOLID, и функция сохранения файл на диск. Вот и все. Конечный результат чистый и компактный, его легко воссоздать для любого другого языка и платформы, и его несложно внедрить в существующий проект, что обеспечивает легкий прирост производительности.