Evitar las trampas del código en línea automáticamente
Publicado: 2022-03-10 La inserción es el proceso de incluir el contenido de los archivos directamente en el documento HTML: los archivos CSS pueden incorporarse dentro de un elemento de style
y los archivos JavaScript pueden incorporarse dentro de un elemento de secuencia de script
:
<style> /* CSS contents here */ </style> <script> /* JS contents here */ </script>
Al imprimir el código que ya está en la salida HTML, la inserción evita las solicitudes de bloqueo de procesamiento y ejecuta el código antes de que se procese la página. Como tal, es útil para mejorar el rendimiento percibido del sitio (es decir, el tiempo que tarda una página en volverse utilizable). Por ejemplo, podemos usar el búfer de datos entregados inmediatamente al cargar el sitio (alrededor de 14 kb) para en línea los estilos críticos, incluidos los estilos del contenido de la mitad superior de la página (como se había hecho en el sitio anterior de Smashing Magazine), y los tamaños de fuente y los anchos y altos del diseño para evitar que el diseño se reprodujera con saltos cuando se entregue el resto de los datos. .
Sin embargo, cuando se exagera, el código insertado también puede tener efectos negativos en el rendimiento del sitio: debido a que el código no se puede almacenar en caché, el mismo contenido se envía al cliente repetidamente y no se puede almacenar previamente en caché a través de Service Workers, o almacenados en caché y accedidos desde una red de entrega de contenido. Además, los scripts en línea no se consideran seguros cuando se implementa una Política de seguridad de contenido (CSP). Luego, es una estrategia sensata alinear esas partes críticas de CSS y JS que hacen que el sitio se cargue más rápido pero que se evitan tanto como sea posible.
Con el objetivo de evitar la inserción en línea, en este artículo exploraremos cómo convertir código en línea en activos estáticos: en lugar de imprimir el código en la salida HTML, lo guardamos en el disco (creando efectivamente un archivo estático) y agregamos el <script>
correspondiente <script>
o etiqueta <link>
para cargar el archivo.
¡Empecemos!
Lectura recomendada : La seguridad de WordPress como proceso
Cuándo evitar en línea
No existe una receta mágica para establecer si algún código debe estar en línea o no, sin embargo, puede ser bastante evidente cuándo algún código no debe estar en línea: cuando involucra una gran parte del código y cuando no se necesita de inmediato.
Como ejemplo, los sitios de WordPress integran las plantillas de JavaScript para representar el Administrador de medios (accesible en la página de la Biblioteca de medios en /wp-admin/upload.php
), imprimiendo una cantidad considerable de código:
Ocupando 43kb completos, el tamaño de este fragmento de código no es insignificante y, dado que se encuentra en la parte inferior de la página, no se necesita de inmediato. Por lo tanto, tendría mucho sentido servir este código a través de activos estáticos en lugar de imprimirlo dentro de la salida HTML.
Veamos a continuación cómo transformar el código en línea en activos estáticos.
Activación de la creación de archivos estáticos
Si los contenidos (los que se van a insertar) provienen de un archivo estático, entonces no hay mucho que hacer más que simplemente solicitar ese archivo estático en lugar de insertar el código.
Sin embargo, para el código dinámico, debemos planificar cómo y cuándo generar el archivo estático con su contenido. Por ejemplo, si el sitio ofrece opciones de configuración (como cambiar el esquema de colores o la imagen de fondo), ¿cuándo se debe generar el archivo que contiene los nuevos valores? Tenemos las siguientes oportunidades para crear los archivos estáticos a partir del código dinámico:
- Bajo pedido
Cuando un usuario accede al contenido por primera vez. - en el cambio
Cuando la fuente del código dinámico (por ejemplo, un valor de configuración) ha cambiado.
Consideremos primero a pedido. La primera vez que un usuario accede al sitio, digamos a través de /index.html
, el archivo estático (por ejemplo header-colors.css
) aún no existe, por lo que debe generarse en ese momento. La secuencia de eventos es la siguiente:
- El usuario solicita
/index.html
; - Al procesar la solicitud, el servidor verifica si existe el archivo
header-colors.css
. Como no lo tiene, obtiene el código fuente y genera el archivo en disco; - Devuelve una respuesta al cliente, incluida la etiqueta
<link rel="stylesheet" type="text/css" href="/staticfiles/header-colors.css">
- El navegador obtiene todos los recursos incluidos en la página, incluido
header-colors.css
; - Para entonces este archivo existe, por lo que se sirve.
Sin embargo, la secuencia de eventos también podría ser diferente, lo que conduciría a un resultado insatisfactorio. Por ejemplo:
- El usuario solicita
/index.html
; - Este archivo ya está almacenado en caché por el navegador (o algún otro proxy, o a través de Service Workers), por lo que la solicitud nunca se envía al servidor;
- El navegador obtiene todos los recursos incluidos en la página, incluido
header-colors.css
. Sin embargo, esta imagen no se almacena en caché en el navegador, por lo que la solicitud se envía al servidor; - El servidor aún no ha generado
header-colors.css
(por ejemplo, se acaba de reiniciar); - Devolverá un 404.
Alternativamente, podríamos generar header-colors.css
no al solicitar /index.html
, sino al solicitar /header-colors.css
en sí mismo. Sin embargo, dado que este archivo inicialmente no existe, la solicitud ya se trata como un 404. Aunque podríamos sortearlo alterando los encabezados para cambiar el código de estado a 200 y devolver el contenido de la imagen, esta es una forma terrible de hacer las cosas, por lo que no consideraremos esta posibilidad (¡somos mucho mejores que esto!)
Eso deja solo una opción: generar el archivo estático después de que su fuente haya cambiado.
Crear el archivo estático cuando cambia la fuente
Tenga en cuenta que podemos crear código dinámico a partir de fuentes que dependen del usuario y del sitio. Por ejemplo, si el tema permite cambiar la imagen de fondo del sitio y el administrador del sitio configura esa opción, el archivo estático se puede generar como parte del proceso de implementación. Por otro lado, si el sitio permite a sus usuarios cambiar la imagen de fondo de sus perfiles, entonces el archivo estático debe generarse en tiempo de ejecución.
En pocas palabras, tenemos estos dos casos:
- Configuración de usuario
El proceso debe activarse cuando el usuario actualiza una configuración. - Configuración del sitio
El proceso debe activarse cuando el administrador actualiza una configuración para el sitio o antes de implementar el sitio.
Si consideráramos los dos casos de forma independiente, para el n.º 2 podríamos diseñar el proceso en cualquier pila de tecnología que quisiéramos. Sin embargo, no queremos implementar dos soluciones diferentes, sino una solución única que pueda abordar ambos casos. Y debido a que, desde el punto 1, el proceso para generar el archivo estático debe activarse en el sitio en ejecución, entonces es convincente diseñar este proceso en torno a la misma pila de tecnología en la que se ejecuta el sitio.
Al diseñar el proceso, nuestro código deberá manejar las circunstancias específicas tanto del n.º 1 como del n.º 2:
- Versionado
Se debe acceder al archivo estático con un parámetro de "versión", para invalidar el archivo anterior al crear un nuevo archivo estático. Mientras que el n.º 2 podría simplemente tener el mismo control de versiones que el sitio, el n.º 1 necesita usar una versión dinámica para cada usuario, posiblemente guardada en la base de datos. - Ubicación del archivo generado
#2 genera un archivo estático único para todo el sitio (por ejemplo,/staticfiles/header-colors.css
), mientras que #1 crea un archivo estático para cada usuario (por ejemplo/staticfiles/users/leo/header-colors.css
). - Evento desencadenante
Mientras que para el n.° 1 el archivo estático debe ejecutarse en tiempo de ejecución, para el n.° 2 también se puede ejecutar como parte de un proceso de compilación en nuestro entorno de prueba. - Despliegue y distribución
Los archivos estáticos en el n.º 2 se pueden integrar perfectamente dentro del paquete de implementación del sitio, sin presentar desafíos; Sin embargo, los archivos estáticos en el n. ° 1 no pueden, por lo que el proceso debe manejar preocupaciones adicionales, como múltiples servidores detrás de un balanceador de carga (¿los archivos estáticos se crearán en 1 servidor solamente, o en todos ellos, y cómo?).
A continuación, diseñemos e implementemos el proceso. Para que se genere cada archivo estático, debemos crear un objeto que contenga los metadatos del archivo, calcular su contenido a partir de las fuentes dinámicas y, finalmente, guardar el archivo estático en el disco. Como caso de uso para guiar las explicaciones a continuación, generaremos los siguientes archivos estáticos:
-
header-colors.css
, con algo de estilo de los valores guardados en la base de datos -
welcomeuser-data.js
, que contiene un objeto JSON con datos de usuario bajo alguna variable:window.welcomeUserData = {name: "Leo"};
.
A continuación, describiré el proceso para generar los archivos estáticos para WordPress, para lo cual debemos basar la pila en las funciones de PHP y WordPress. La función para generar los archivos estáticos antes de la implementación se puede activar cargando una página especial que ejecuta el código [create_static_files]
como lo describí en un artículo anterior.
Otras lecturas recomendadas : Hacer un trabajador de servicios: un estudio de caso
Representando el archivo como un objeto
Debemos modelar un archivo como un objeto PHP con todas las propiedades correspondientes, de modo que podamos guardar el archivo en el disco en una ubicación específica (por ejemplo, en /staticfiles/
o /staticfiles/users/leo/
), y saber cómo solicitar el archivo en consecuencia. Para ello, creamos un Resource
interfaz que devuelve tanto los metadatos del archivo (nombre de archivo, directorio, tipo: “css” o “js”, versión y dependencias de otros recursos) como su contenido.
interface Resource { function get_filename(); function get_dir(); function get_type(); function get_version(); function get_dependencies(); function get_content(); }
Para hacer que el código sea mantenible y reutilizable, seguimos los principios SOLID, para lo cual establecemos un esquema de herencia de objetos para que los recursos agreguen propiedades gradualmente, comenzando desde la clase abstracta ResourceBase
de la cual heredarán todas nuestras implementaciones de recursos:
abstract class ResourceBase implements Resource { function get_dependencies() { // By default, a file has no dependencies return array(); } }
Siguiendo SOLID, creamos subclases cada vez que las propiedades difieren. Como se indicó anteriormente, la ubicación del archivo estático generado y el control de versiones para solicitarlo serán diferentes según el archivo sea sobre el usuario o la configuración del sitio:
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, en el último nivel, implementamos los objetos para los archivos que queremos generar, agregando el nombre del archivo, el tipo de archivo y el código dinámico a través de la función 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 ) ) ); } }
Con esto, hemos modelado el archivo como un objeto PHP. A continuación, debemos guardarlo en el disco.
Guardar el archivo estático en el disco
Guardar un archivo en el disco se puede lograr fácilmente a través de las funciones nativas proporcionadas por el lenguaje. En el caso de PHP, esto se logra a través de la función fwrite
. Además, creamos una clase de utilidad ResourceUtils
con funciones que proporcionan la ruta absoluta al archivo en el disco y también su ruta relativa a la raíz del sitio:
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); } }
Luego, cada vez que la fuente cambia y el archivo estático necesita ser regenerado, ejecutamos ResourceGenerator::save
pasando el objeto que representa el archivo como parámetro. El siguiente código regenera y guarda en el disco los archivos "header-colors.css" y "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());
Una vez que existen, podemos poner en cola los archivos para que se carguen a través de las etiquetas <script>
y <link>
.
Poner en cola los archivos estáticos
Poner en cola los archivos estáticos no es diferente a poner en cola cualquier recurso en WordPress: a través de las funciones wp_enqueue_script
y wp_enqueue_style
. Luego, simplemente iteramos todas las instancias del objeto y usamos un gancho u otro dependiendo de que su valor get_type()
sea "js"
o "css"
.
Primero agregamos funciones de utilidad para proporcionar la URL del archivo y para indicar que el tipo es JS o 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"; } }
Una instancia de la clase ResourceEnqueuer
contendrá todos los archivos que deben cargarse; cuando se invoca, sus funciones enqueue_scripts
y enqueue_styles
harán la puesta en cola, ejecutando las funciones correspondientes de WordPress ( wp_enqueue_script
y 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); } } }
Finalmente, creamos una instancia de un objeto de la clase ResourceEnqueuer
con una lista de los objetos PHP que representan cada archivo y agregamos un gancho de WordPress para ejecutar la puesta en cola:
// 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'));
Eso es todo: al estar en cola, los archivos estáticos se solicitarán al cargar el sitio en el cliente. Hemos logrado evitar imprimir código en línea y cargar recursos estáticos en su lugar.
A continuación, podemos aplicar varias mejoras para obtener ganancias de rendimiento adicionales.
Lectura recomendada : una introducción a las pruebas automatizadas de complementos de WordPress con PHPUnit
Agrupación de archivos
Aunque HTTP/2 ha reducido la necesidad de agrupar archivos, aún hace que el sitio sea más rápido, porque la compresión de archivos (por ejemplo, a través de GZip) será más efectiva y porque los navegadores (como Chrome) tienen una sobrecarga mayor al procesar muchos recursos. .
Por ahora, hemos modelado un archivo como un objeto PHP, lo que nos permite tratar este objeto como una entrada para otros procesos. En particular, podemos repetir el mismo proceso anterior para agrupar todos los archivos del mismo tipo y servir la versión agrupada en lugar de todos los archivos independientes. Para esto, creamos una función get_content
que simplemente extrae el contenido de cada recurso bajo $fileObjects
y lo imprime nuevamente, produciendo la agregación de todo el contenido de todos los 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 los archivos en el archivo bundled-styles.css
creando una clase para este archivo:
class StylesSiteBundle extends SiteBundleBase { function get_filename() { return "bundled-styles"; } function get_type() { return "css"; } }
Finalmente, simplemente ponemos en cola estos archivos agrupados, como antes, en lugar de todos los recursos independientes. Para CSS, creamos un paquete que contiene los archivos header-colors.css
, background-image.css
y font-sizes.css
, para lo cual simplemente creamos una instancia de StylesSiteBundle
con el objeto PHP para cada uno de estos archivos (y del mismo modo podemos crear el JS archivo de paquete):
$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) ) );
Eso es todo. Ahora solicitaremos solo un archivo JS y un archivo CSS en lugar de muchos.
Una mejora final para el rendimiento percibido implica priorizar los activos, al retrasar la carga de aquellos activos que no se necesitan de inmediato. Abordemos esto a continuación.
async
/ defer
Atributos para recursos JS
Podemos agregar atributos async
y defer
a la etiqueta <script>
, para modificar cuándo se descarga, analiza y ejecuta el archivo JavaScript, para priorizar el JavaScript crítico y empujar todo lo que no es crítico lo más tarde posible, disminuyendo así la carga aparente del sitio. hora.
Para implementar esta función, siguiendo los principios de SOLID, debemos crear una nueva interfaz JSResource
(que hereda de Resource
) que contenga las funciones is_async
e is_defer
. Sin embargo, esto cerraría la puerta a las etiquetas <style>
que finalmente admitirían estos atributos también. Entonces, con la adaptabilidad en mente, adoptamos un enfoque más abierto: simplemente agregamos un método genérico get_attributes
a la interfaz Resource
para mantenerlo flexible para agregar a cualquier atributo (ya sea existente o aún por inventar) para ambos <script>
etiquetas <script>
y <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 no ofrece una manera fácil de agregar atributos adicionales a los recursos en cola, por lo que lo hacemos de una manera bastante complicada, agregando un enlace que reemplaza una cadena dentro de la etiqueta a través de la función 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 );
Agregamos los atributos para un recurso al crear la instancia de objeto correspondiente:
abstract class ResourceBase implements Resource { // Continued from above... function __construct() { ResourceEnqueuerUtils::add_tag_attributes($this->get_filename(), $this->get_attributes()); } }
Finalmente, si el recurso welcomeuser-data.js
no necesita ejecutarse de inmediato, podemos configurarlo como defer
:
class WelcomeUserDataUserResource extends UserResourceBase { // Continued from above... function get_attributes() { return "defer='defer'"; } }
Debido a que se carga como diferido, un script se cargará más tarde, adelantando el momento en el que el usuario puede interactuar con el sitio. Con respecto a las ganancias de rendimiento, ¡ya estamos listos!
Queda un problema por resolver antes de que podamos relajarnos: ¿qué sucede cuando el sitio está alojado en varios servidores?
Manejo de múltiples servidores detrás de un balanceador de carga
Si nuestro sitio está alojado en varios sitios detrás de un balanceador de carga y se regenera un archivo dependiente de la configuración del usuario, el servidor que maneja la solicitud debe, de alguna manera, cargar el archivo estático regenerado a todos los demás servidores; de lo contrario, los otros servidores servirán una versión obsoleta de ese archivo a partir de ese momento. Cómo hacemos esto? Hacer que los servidores se comuniquen entre sí no solo es complejo, sino que en última instancia puede resultar inviable: ¿Qué sucede si el sitio se ejecuta en cientos de servidores, de diferentes regiones? Claramente, esto no es una opción.
La solución que se me ocurrió es agregar un nivel de direccionamiento indirecto: en lugar de solicitar los archivos estáticos de la URL del sitio, se solicitan desde una ubicación en la nube, como un depósito de AWS S3. Luego, al regenerar el archivo, el servidor cargará inmediatamente el nuevo archivo en S3 y lo servirá desde allí. La implementación de esta solución se explica en mi artículo anterior Compartir datos entre varios servidores a través de AWS S3.
Conclusión
En este artículo, hemos considerado que insertar código JS y CSS no siempre es ideal, porque el código debe enviarse repetidamente al cliente, lo que puede afectar el rendimiento si la cantidad de código es significativa. Vimos, a modo de ejemplo, cómo WordPress carga 43kb de scripts para imprimir el Media Manager, que son plantillas de JavaScript puro y perfectamente podrían cargarse como recursos estáticos.
Por lo tanto, hemos ideado una forma de hacer que el sitio web sea más rápido al transformar el código en línea dinámico JS y CSS en recursos estáticos, lo que puede mejorar el almacenamiento en caché en varios niveles (en el cliente, Service Workers, CDN), permite agrupar aún más todos los archivos juntos en un solo recurso JS/CSS para mejorar la proporción al comprimir la salida (como a través de GZip) y evitar una sobrecarga en los navegadores al procesar varios recursos al mismo tiempo (como en Chrome), y además permite agregar atributos async
o defer
a la etiqueta <script>
para acelerar la interactividad del usuario, mejorando así el tiempo aparente de carga del sitio.
Como efecto secundario beneficioso, dividir el código en recursos estáticos también permite que el código sea más legible, ya que trata con unidades de código en lugar de grandes blobs de HTML, lo que puede conducir a un mejor mantenimiento del proyecto.
La solución que desarrollamos se hizo en PHP e incluye algunos bits de código específicos para WordPress, sin embargo, el código en sí es extremadamente simple, apenas unas pocas interfaces que definen propiedades y objetos que implementan esas propiedades siguiendo los principios SOLID, y una función para guardar un archivo a disco. Eso es practicamente todo. El resultado final es limpio y compacto, fácil de recrear para cualquier otro lenguaje y plataforma, y no es difícil de introducir en un proyecto existente, lo que proporciona ganancias de rendimiento sencillas.