Evitando as armadilhas do código embutido automaticamente

Publicados: 2022-03-10
Resumo rápido ↬ O uso excessivo de código CSS ou JS embutido, em vez de servir código por meio de recursos estáticos, pode prejudicar o desempenho do site. Neste artigo, aprenderemos como carregar código dinâmico por meio de arquivos estáticos, evitando as desvantagens de muito código embutido.

Inlining é o processo de incluir o conteúdo de arquivos diretamente no documento HTML: arquivos CSS podem ser embutidos em um elemento de style e arquivos JavaScript podem ser embutidos em um elemento de script :

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

Ao imprimir o código já na saída HTML, o inlining evita solicitações de bloqueio de renderização e executa o código antes que a página seja renderizada. Como tal, é útil para melhorar o desempenho percebido do site (ou seja, o tempo que leva para uma página se tornar utilizável). Por exemplo, podemos usar o buffer de dados entregues imediatamente ao carregar o site (cerca de 14kb) os estilos críticos, incluindo estilos de conteúdo acima da dobra (como foi feito no site anterior da Smashing Magazine), e tamanhos de fonte e larguras e alturas de layout para evitar uma nova renderização de layout irregular quando o restante dos dados for entregue .

No entanto, quando exagerado, o código embutido também pode ter efeitos negativos no desempenho do site: como o código não pode ser armazenado em cache, o mesmo conteúdo é enviado ao cliente repetidamente e não pode ser pré-armazenado em cache por meio de Service Workers ou armazenados em cache e acessados ​​de uma rede de entrega de conteúdo. Além disso, os scripts embutidos não são considerados seguros ao implementar uma Política de Segurança de Conteúdo (CSP). Então, é uma estratégia sensata para inline aquelas partes críticas de CSS e JS que fazem o site carregar mais rápido, mas evitadas tanto quanto possível.

Com o objetivo de evitar inlining, neste artigo vamos explorar como converter código inline em ativos estáticos: Em vez de imprimir o código na saída HTML, nós o salvamos em disco (efetivamente criando um arquivo estático) e adicionamos o <script> correspondente <script> ou <link> para carregar o arquivo.

Vamos começar!

Leitura recomendada : Segurança do WordPress como um processo

Mais depois do salto! Continue lendo abaixo ↓

Quando evitar o inline

Não existe uma receita mágica para estabelecer se algum código deve ser embutido ou não, no entanto, pode ser bastante evidente quando algum código não deve ser embutido: quando envolve um grande pedaço de código e quando não é necessário imediatamente.

Como exemplo, os sites do WordPress inline os modelos JavaScript para renderizar o Gerenciador de mídia (acessível na página Biblioteca de mídia em /wp-admin/upload.php ), imprimindo uma quantidade considerável de código:

Uma captura de tela do código-fonte da página Biblioteca de mídia
Modelos JavaScript embutidos pelo WordPress Media Manager.

Ocupando 43kb completos, o tamanho desse pedaço de código não é desprezível e, como fica na parte inferior da página, não é necessário imediatamente. Portanto, faria muito sentido servir esse código por meio de ativos estáticos ou imprimi-lo dentro da saída HTML.

Vamos ver a seguir como transformar código embutido em ativos estáticos.

Acionando a criação de arquivos estáticos

Se o conteúdo (aqueles a serem embutidos) vem de um arquivo estático, então não há muito a fazer além de simplesmente solicitar esse arquivo estático em vez de inserir o código.

Para código dinâmico, porém, devemos planejar como/quando gerar o arquivo estático com seu conteúdo. Por exemplo, se o site oferece opções de configuração (como alterar o esquema de cores ou a imagem de fundo), quando deve ser gerado o arquivo contendo os novos valores? Temos as seguintes oportunidades para criar os arquivos estáticos do código dinâmico:

  1. A pedido
    Quando um usuário acessa o conteúdo pela primeira vez.
  2. Em mudança
    Quando a origem do código dinâmico (por exemplo, um valor de configuração) foi alterada.

Vamos considerar a pedido primeiro. A primeira vez que um usuário acessa o site, digamos através de /index.html , o arquivo estático (por exemplo header-colors.css ) ainda não existe, então ele deve ser gerado então. A sequência de eventos é a seguinte:

  1. O usuário solicita /index.html ;
  2. Ao processar a solicitação, o servidor verifica se o arquivo header-colors.css existe. Como não tem, obtém o código fonte e gera o arquivo em disco;
  3. Ele retorna uma resposta ao cliente, incluindo a tag <link rel="stylesheet" type="text/css" href="/staticfiles/header-colors.css">
  4. O navegador busca todos os recursos incluídos na página, incluindo header-colors.css ;
  5. Até então este arquivo existe, então ele é servido.

No entanto, a sequência de eventos também pode ser diferente, levando a um resultado insatisfatório. Por exemplo:

  1. O usuário solicita /index.html ;
  2. Esse arquivo já está armazenado em cache pelo navegador (ou algum outro proxy, ou por meio de Service Workers), portanto, a solicitação nunca é enviada ao servidor;
  3. O navegador busca todos os recursos incluídos na página, incluindo header-colors.css . Essa imagem, no entanto, não é armazenada em cache no navegador, portanto, a solicitação é enviada ao servidor;
  4. O servidor ainda não gerou header-colors.css (por exemplo, acabou de ser reiniciado);
  5. Ele retornará um 404.

Alternativamente, poderíamos gerar header-colors.css não ao solicitar /index.html , mas ao solicitar o próprio /header-colors.css . No entanto, como esse arquivo inicialmente não existe, a solicitação já é tratada como 404. Mesmo que pudéssemos contornar isso, alterando os cabeçalhos para alterar o código de status para 200 e retornando o conteúdo da imagem, esta é uma maneira terrível de fazer as coisas, então não vamos considerar essa possibilidade (somos muito melhores do que isso!)

Isso deixa apenas uma opção: gerar o arquivo estático depois que sua fonte for alterada.

Criando o arquivo estático quando a fonte muda

Observe que podemos criar código dinâmico de fontes dependentes do usuário e dependentes do site. Por exemplo, se o tema permitir alterar a imagem de plano de fundo do site e essa opção for configurada pelo administrador do site, o arquivo estático poderá ser gerado como parte do processo de implantação. Por outro lado, se o site permite que seus usuários alterem a imagem de fundo de seus perfis, o arquivo estático deve ser gerado em tempo de execução.

Em poucas palavras, temos estes dois casos:

  1. Configuração do usuário
    O processo deve ser acionado quando o usuário atualiza uma configuração.
  2. Configuração do site
    O processo deve ser acionado quando o administrador atualiza uma configuração para o site ou antes de implantar o site.

Se considerarmos os dois casos independentemente, para o nº 2 poderíamos projetar o processo em qualquer pilha de tecnologia que quiséssemos. No entanto, não queremos implementar duas soluções diferentes, mas uma solução única que possa resolver os dois casos. E como a partir do número 1 o processo para gerar o arquivo estático deve ser acionado no site em execução, é interessante projetar esse processo em torno da mesma pilha de tecnologia em que o site é executado.

Ao projetar o processo, nosso código precisará lidar com as circunstâncias específicas de #1 e #2:

  • Controle de versão
    O arquivo estático deve ser acessado com um parâmetro “version”, para invalidar o arquivo anterior na criação de um novo arquivo estático. Enquanto o nº 2 poderia simplesmente ter o mesmo versionamento do site, o nº 1 precisa usar uma versão dinâmica para cada usuário, possivelmente salva no banco de dados.
  • Localização do arquivo gerado
    O #2 gera um arquivo estático exclusivo para todo o site (por exemplo, /staticfiles/header-colors.css ), enquanto o #1 cria um arquivo estático para cada usuário (por exemplo /staticfiles/users/leo/header-colors.css ).
  • Evento acionador
    Enquanto para #1 o arquivo estático deve ser executado em tempo de execução, para #2 ele também pode ser executado como parte de um processo de construção em nosso ambiente de teste.
  • Implantação e distribuição
    Os arquivos estáticos no nº 2 podem ser integrados perfeitamente ao pacote de implantação do site, sem apresentar desafios; os arquivos estáticos em #1, no entanto, não podem, portanto, o processo deve lidar com preocupações adicionais, como vários servidores atrás de um balanceador de carga (os arquivos estáticos serão criados em apenas 1 servidor ou em todos eles e como?).

Vamos projetar e implementar o processo a seguir. Para cada arquivo estático a ser gerado devemos criar um objeto contendo os metadados do arquivo, calcular seu conteúdo a partir das fontes dinâmicas e finalmente salvar o arquivo estático em disco. Como um caso de uso para orientar as explicações abaixo, geraremos os seguintes arquivos estáticos:

  1. header-colors.css , com algum estilo de valores salvos no banco de dados
  2. welcomeuser-data.js , contendo um objeto JSON com dados do usuário em alguma variável: window.welcomeUserData = {name: "Leo"}; .

Abaixo, descreverei o processo para gerar os arquivos estáticos para WordPress, para o qual devemos basear a pilha em funções PHP e WordPress. A função para gerar os arquivos estáticos antes da implantação pode ser acionada carregando uma página especial executando o shortcode [create_static_files] como descrevi em um artigo anterior.

Leitura recomendada adicional : Fazendo um trabalhador de serviço: um estudo de caso

Representando o arquivo como um objeto

Devemos modelar um arquivo como um objeto PHP com todas as propriedades correspondentes, para que possamos salvar o arquivo no disco em um local específico (por exemplo, em /staticfiles/ ou /staticfiles/users/leo/ ), e saber como solicitar o arquivo consequentemente. Para isso, criamos uma interface Resource retornando tanto os metadados do arquivo (nome do arquivo, dir, tipo: “css” ou “js”, versão e dependências de outros recursos) quanto seu conteúdo.

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

Para tornar o código sustentável e reutilizável, seguimos os princípios SOLID, para os quais definimos um esquema de herança de objetos para recursos para adicionar propriedades gradualmente, começando pela classe abstrata ResourceBase da qual todas as nossas implementações de recursos herdarão:

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

Seguindo o SOLID, criamos subclasses sempre que as propriedades diferem. Como dito anteriormente, a localização do arquivo estático gerado e o versionamento para solicitá-lo serão diferentes dependendo do arquivo sobre a configuração do usuário ou do site:

 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; } }

Finalmente, no último nível, implementamos os objetos para os arquivos que queremos gerar, adicionando o nome do arquivo, o tipo de arquivo e o código dinâmico através da função 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 ) ) ); } }

Com isso, modelamos o arquivo como um objeto PHP. Em seguida, precisamos salvá-lo em disco.

Salvando o arquivo estático em disco

Salvar um arquivo em disco pode ser feito facilmente através das funções nativas fornecidas pelo idioma. No caso do PHP, isso é feito através da função fwrite . Além disso, criamos uma classe utilitária ResourceUtils com funções que fornecem o caminho absoluto para o arquivo no disco e também seu caminho relativo à raiz do site:

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

Então, sempre que a fonte for alterada e o arquivo estático precisar ser regenerado, executamos ResourceGenerator::save passando o objeto que representa o arquivo como parâmetro. O código abaixo regenera e salva em disco os arquivos “header-colors.css” e “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());

Uma vez que eles existam, podemos enfileirar os arquivos a serem carregados através das tags <script> e <link> .

Enfileirando os arquivos estáticos

Enfileirar os arquivos estáticos não é diferente de enfileirar qualquer recurso no WordPress: através das funções wp_enqueue_script e wp_enqueue_style . Então, simplesmente iteramos todas as instâncias do objeto e usamos um gancho ou outro dependendo do valor de get_type() ser "js" ou "css" .

Primeiro, adicionamos funções utilitárias para fornecer a URL do arquivo e informar o tipo como JS ou 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"; } }

Uma instância da classe ResourceEnqueuer conterá todos os arquivos que devem ser carregados; quando invocadas, suas funções enqueue_scripts e enqueue_styles farão o enfileiramento, executando as funções correspondentes do WordPress ( wp_enqueue_script e wp_enqueue_style respectivamente):

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

Por fim, instanciamos um objeto da classe ResourceEnqueuer com uma lista dos objetos PHP que representam cada arquivo e adicionamos um hook do WordPress para executar o enfileiramento:

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

É isso: Sendo enfileirado, os arquivos estáticos serão solicitados ao carregar o site no cliente. Conseguimos evitar imprimir código embutido e carregar recursos estáticos.

Em seguida, podemos aplicar várias melhorias para ganhos de desempenho adicionais.

Leitura recomendada : Uma introdução ao teste automatizado de plugins do WordPress com PHPUnit

Agrupando arquivos juntos

Embora o HTTP/2 tenha reduzido a necessidade de empacotar arquivos, ele ainda torna o site mais rápido, porque a compactação de arquivos (por exemplo, através do GZip) será mais eficaz e porque os navegadores (como o Chrome) têm uma sobrecarga maior processando muitos recursos .

Até agora, modelamos um arquivo como um objeto PHP, o que nos permite tratar esse objeto como uma entrada para outros processos. Em particular, podemos repetir o mesmo processo acima para agrupar todos os arquivos do mesmo tipo e servir a versão agrupada em vez de todos os arquivos independentes. Para isso, criamos uma função get_content que simplesmente extrai o conteúdo de cada recurso em $fileObjects e o imprime novamente, produzindo a agregação de todo o conteúdo de todos os recursos:

 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; } }

Podemos agrupar todos os arquivos no arquivo bundled-styles.css criando uma classe para este arquivo:

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

Por fim, simplesmente enfileiramos esses arquivos agrupados, como antes, em vez de todos os recursos independentes. Para CSS, criamos um pacote contendo os arquivos header-colors.css , background-image.css e font-sizes.css , para os quais simplesmente instanciamos StylesSiteBundle com o objeto PHP para cada um desses arquivos (e da mesma forma podemos criar o JS arquivo de pacote):

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

É isso. Agora estaremos solicitando apenas um arquivo JS e um arquivo CSS em vez de muitos.

Uma melhoria final para o desempenho percebido envolve a priorização de ativos, atrasando o carregamento daqueles ativos que não são necessários imediatamente. Vamos abordar isso a seguir.

async / defer atributos para recursos JS

Podemos adicionar atributos async e defer para a tag <script> , para alterar quando o arquivo JavaScript é baixado, analisado e executado, para priorizar JavaScript crítico e enviar tudo o que não é crítico o mais tarde possível, diminuindo assim o carregamento aparente do site Tempo.

Para implementar este recurso, seguindo os princípios SOLID, devemos criar uma nova interface JSResource (que herda de Resource ) contendo as funções is_async e is_defer . No entanto, isso fecharia a porta para as tags <style> eventualmente suportarem esses atributos também. Assim, com a adaptabilidade em mente, adotamos uma abordagem mais aberta: simplesmente adicionamos um método genérico get_attributes à interface Resource para mantê-lo flexível para adicionar a qualquer atributo (já existente ou ainda a ser inventado) para ambos <script> e tags <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 ''; } }

O WordPress não oferece uma maneira fácil de adicionar atributos extras aos recursos enfileirados, então fazemos isso de uma maneira bastante hacky, adicionando um gancho que substitui uma string dentro da tag por meio da função 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 );

Adicionamos os atributos de um recurso ao criar a instância de objeto correspondente:

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

Por fim, se o recurso welcomeuser-data.js não precisar ser executado imediatamente, podemos defini-lo como defer :

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

Por ser carregado como adiado, um script será carregado posteriormente, antecipando o momento em que o usuário pode interagir com o site. Em relação aos ganhos de desempenho, estamos prontos agora!

Há um problema a ser resolvido antes que possamos relaxar: o que acontece quando o site é hospedado em vários servidores?

Lidando com vários servidores por trás de um balanceador de carga

Se nosso site estiver hospedado em vários sites por trás de um balanceador de carga e um arquivo dependente de configuração do usuário for gerado novamente, o servidor que trata a solicitação deve, de alguma forma, carregar o arquivo estático regenerado para todos os outros servidores; caso contrário, os outros servidores servirão uma versão antiga desse arquivo a partir desse momento. Como vamos fazer isso? Fazer com que os servidores se comuniquem não é apenas complexo, mas pode acabar se mostrando inviável: o que acontece se o site for executado em centenas de servidores, de diferentes regiões? Claramente, esta não é uma opção.

A solução que encontrei é adicionar um nível de indireção: em vez de solicitar os arquivos estáticos da URL do site, eles são solicitados de um local na nuvem, como um bucket do AWS S3. Então, ao gerar novamente o arquivo, o servidor fará o upload imediato do novo arquivo para o S3 e o servirá a partir daí. A implementação dessa solução é explicada em meu artigo anterior Compartilhando dados entre vários servidores por meio do AWS S3.

Conclusão

Neste artigo, consideramos que o código JS e CSS embutido nem sempre é o ideal, pois o código deve ser enviado repetidamente ao cliente, o que pode afetar o desempenho se a quantidade de código for significativa. Vimos, por exemplo, como o WordPress carrega 43kb de scripts para imprimir o Media Manager, que são templates JavaScript puros e podem perfeitamente ser carregados como recursos estáticos.

Assim, criamos uma maneira de tornar o site mais rápido, transformando o código dinâmico JS e CSS inline em recursos estáticos, o que pode melhorar o cache em vários níveis (no cliente, Service Workers, CDN), permite agrupar ainda mais todos os arquivos em apenas um recurso JS/CSS para melhorar a proporção ao compactar a saída (como por meio de GZip) e evitar uma sobrecarga nos navegadores ao processar vários recursos simultaneamente (como no Chrome), além de permitir adicionar atributos async ou defer à tag <script> para agilizar a interatividade do usuário, melhorando o tempo de carregamento aparente do site.

Como efeito colateral benéfico, dividir o código em recursos estáticos também permite que o código fique mais legível, lidando com unidades de código em vez de grandes bolhas de HTML, o que pode levar a uma melhor manutenção do projeto.

A solução que desenvolvemos foi feita em PHP e inclui alguns bits específicos de código para WordPress, porém, o código em si é extremamente simples, poucas interfaces definindo propriedades e objetos implementando essas propriedades seguindo os princípios SOLID, e uma função para salvar um arquivo para o disco. É quase isso. O resultado final é limpo e compacto, fácil de recriar para qualquer outro idioma e plataforma e não é difícil de introduzir em um projeto existente — proporcionando ganhos de desempenho fáceis.