Как избежать ловушек автоматически подстиланного кода

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

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

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

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

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

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

Начнем!

Рекомендуемое чтение: WordPress безопасности как процесс

Когда, чтобы избежать Inlining

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

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

A screenshot of the source code for the Media Library page
Шаблоны JavaScript, встроенные медиа-менеджером WordPress.

Занимая полный 43kb, размер этого фрагмента кода не является незначительным, и так как он сидит в нижней части страницы это не нужно сразу. Таким образом, было бы достаточно смысла служить этому коду через статические активы, а не или печатать его внутри 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. Этот файл уже кэшируется браузером (или каким-либо другим прокси, или через работников службы), поэтому запрос никогда не отправляется на сервер;
  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. Функция для генерации статических файлов перед развертыванием может быть запущена путем загрузки специальной страницы выполнения shortcode, [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 из которого все наши реализации Ресурсов унаследует:

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());

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

Enqueuing Статические файлы

Enqueuing статических файлов ничем не отличается от quequeuing любого ресурса в 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 enqueuing, путем выполнения соответствующих функций WordPress wp_enqueue_scriptwp_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 для выполнения queuing:

// 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'));

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

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

Рекомендуемое чтение: Введение в автоматизированное тестирование WordPress плагинов с PHPUnit

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

Несмотря на то, что HTTP/2 уменьшил потребность в комплектации файлов, он по-прежнему делает сайт быстрее, потому что сжатие файлов (например, через ГЗип) будет более эффективным,и потому, что браузеры (такие как 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> обоих и <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 не предлагает простой способ добавить дополнительные атрибуты в enqueued ресурсов, поэтому мы делаем это в довольно хаки образом, добавив крюк, который заменяет строку внутри тега через функцию 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 загружает 43kb скриптов для печати Media Manager, которые являются чистыми шаблонами JavaScript и могут быть полностью загружены как статические ресурсы.

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

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

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

Источник: smashingmagazine.com

Великолепный Журнал

Великолепный, сокрушительный, разящий (см. перевод smashing) независимый журнал о веб-разработке. Основан в 2006 году в Германии. Имеет няшный дизайн и кучу крутых авторов, которых читают 2 млн человек в месяц.

Добавить комментарий

%d такие блоггеры, как: