DevTips: cómo compilar sus activos de manera eficiente

Publicado: 2022-08-25

Uno de los objetivos que teníamos para este año era refactorizar nuestros dos complementos emblemáticos (Nelio Content y Nelio A/B Testing) a TypeScript y React Hooks. Bueno, apenas llevamos medio año y ya podemos decir que este objetivo ha sido todo un éxito. Sin embargo, debo reconocerlo: el camino ha sido un poco más complicado de lo esperado… sobre todo si tenemos en cuenta que, tras introducir TypeScript, nuestro ¡Los tiempos de compilación del complemento pasaron de unos pocos segundos a más de dos minutos! Algo andaba mal y no sabíamos qué.

Bueno, en la publicación de hoy me gustaría contarles un poco sobre esa experiencia y lo que hicimos para solucionarla. Después de todo, todos sabemos que TypeScript siempre ralentizará un poco el proceso de compilación (la verificación de tipos tiene un precio), ¡pero no debería ser tanto! Bueno, alerta de spoiler: el problema nunca fue TypeScript… fue mi configuración. TypeScript solo lo hizo "obvio". Así que empecemos, ¿de acuerdo?

Proyecto de juguete

Para ayudarlo a comprender el problema que encontramos hace unas semanas y cómo lo solucionamos, lo mejor que podemos hacer es crear un ejemplo muy simple para que lo siga. Construyamos un complemento simple de WordPress que use TypeScript y cómo una configuración incorrecta puede resultar en tiempos de compilación extremadamente lentos. Si necesita ayuda para comenzar, consulte esta publicación sobre entornos de desarrollo de WordPress.

Creación de complementos

Lo primero que debes hacer es crear una nueva carpeta con el nombre de tu plugin (por ejemplo, nelio ) en el directorio /wp-content/plugins de tu WordPress. Luego agregue el archivo principal ( nelio.php ) con el siguiente contenido:

 <?php /** * Plugin Name: Nelio * Description: This is a test plugin. * Version: 0.0.1 * * Author: Nelio Software * Author URI: https://neliosoftware.com * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * * Text Domain: nelio */ if ( ! defined( 'ABSPATH' ) ) { exit; }

Si lo has hecho bien, verás que ya puedes activar el plugin en WordPress:

Captura de pantalla de nuestro complemento de juguete en la lista de complementos
Captura de pantalla de nuestro complemento de juguete en la lista de complementos.

Claro, el complemento no hace nada todavía... pero al menos aparece

Mecanografiado

¡Agreguemos algo de código TypeScript! Lo primero que haremos será inicializar npm en nuestra carpeta de complementos. Ejecuta esto:

 npm init

y siga las instrucciones en pantalla. Luego, instala las dependencias:

 npm add -D @wordpress/scripts @wordpress/i18n

y edite el archivo package.json para agregar los scripts de compilación requeridos por @wordpress/scripts :

 { ... "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", }, ... }

Una vez que npm esté listo, personalicemos TypeScript agregando un archivo tsconfig.json :

 { "compilerOptions": { "target": "es5", "module": "esnext", "moduleResolution": "node", "outDir": "build", "lib": [ "es7", "dom" ] }, "exclude": [ "node_modules" ] }

Finalmente, escribamos algo de código TS. Queremos que sea muy simple pero “lo suficientemente cercano” a lo que teníamos en Nelio A/B Testing y Nelio Content, así que crea una carpeta src en nuestro proyecto con un par de archivos TypeScript dentro: index.ts y utils/index.ts .

Por un lado, supongamos que utils/index.ts es un paquete de utilidades. Es decir, contiene algunas funciones que otros archivos de nuestro proyecto podrían necesitar. Por ejemplo, digamos que proporciona las funciones min y max clásicas:

 export const min = ( a: number, b: number ): number => a < b ? a : b; export const max = ( a: number, b: number ): number => a > b ? a : b;

Por otro lado, echemos un vistazo al archivo principal de nuestra aplicación: index.ts . Para nuestros propósitos de prueba, todo lo que queremos es un script simple que use nuestro paquete de utilidades y una dependencia de WordPress. Algo como esto:

 import { __, sprintf } from '@wordpress/i18n'; import { min } from './utils'; const a = 2; const b = 3; const m = min( a, b ); console.log( sprintf( /* translators: 1 -> num, 2 -> num, 3 -> num */ __( 'Min between %1$s and %2$s is %3$s', 'nelio' ), a, b, m ) );

Configuración predeterminada de @wordpress/scripts

Si tuviéramos que construir el proyecto ahora mismo usando npm run build , todo funcionaría de inmediato. Y eso es simplemente porque @wordpress/scripts (es decir, la herramienta subyacente que estamos usando para construir nuestro proyecto) se ha diseñado para funcionar con un código base como el de nuestro ejemplo. Es decir, si tenemos un archivo index.ts en la carpeta src , generará un archivo index.js en la carpeta de build junto con un archivo de dependencia index.asset.php :

 > ls build index.asset.php index.js

¿Por qué dos archivos? Bueno, uno es el archivo JavaScript compilado (duh) y el otro es un archivo de dependencias con información útil sobre nuestro script. En concreto, nos dice en qué bibliotecas de JavaScript, de las incluidas en WordPress, se apoya. Por ejemplo, nuestro index.ts se basa en el paquete @wordpress/i18n para internacionalizar cadenas, y esa es una biblioteca incluida en WordPress, así que... sí, wp-i18n aparecerá en index.asset.php :

 build/index.asset.php <?php return array( 'dependencies' => array( 'wp-i18n' ), 'version' => 'c6131c7f24df4fa803b7', );

Desafortunadamente, la configuración predeterminada no es perfecta, si me preguntas. Este es el por qué.

Si introducimos un error en su código (por ejemplo, llamemos a la función min con un argumento de string en lugar de un number ):

 const m = min( `${ a }`, b );

esto debería desencadenar un error. Pero no es así. Compila sin problemas.

Comprobaciones de tipo durante la compilación con TypeScript

Para resolver la "limitación" antes mencionada, solo necesitamos crear nuestro propio archivo de configuración del paquete web y decirle que use tsc (el compilador de TypeScript) cada vez que encuentre el código TS. En otras palabras, necesitamos el siguiente archivo webpack.config.json :

 const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); const config = { ...defaultConfig, module: { ...defaultConfig.module, rules: [ ...defaultConfig.module.rules, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, }; module.exports = { ...config, entry: './src/index', output: { path: __dirname + '/build', filename: 'index.js', }, };

Como puede ver, comienza cargando la configuración predeterminada del paquete web incluida en el paquete @wordpress/scripts y luego extiende la defaultConfig agregando un ts-loader a todos los archivos .ts . ¡Pan comido!

Y ahora, he aquí:

 > npm run build ... ERROR in ...src/index.ts TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. webpack 5.74.0 compiled with 1 error in 4470 ms

compilar nuestro proyecto da como resultado un error. ¡Hurra! Claro, es un poco más lento, pero al menos tenemos algunos controles de seguridad antes de subir algo a producción.

Poner en cola los scripts

Bueno, ahora que sabe que hay un problema en su código, arréglelo y vuelva a compilar el complemento. ¿Funcionó todo? ¡Enfriar! Porque ahora es el momento de poner en cola el script y sus dependencias en PHP para que podamos probarlo en nuestro navegador.

Abre nelio.php y añade el siguiente fragmento:

 add_action( 'admin_enqueue_scripts', function() { $path = untrailingslashit( plugin_dir_path( __FILE__ ) ); $url = untrailingslashit( plugin_dir_url( __FILE__ ) ); $asset = require( $path . '/build/index.asset.php' ); wp_enqueue_script( 'nelio', $url . '/build/index.js', $asset['dependencies'], $asset['version'] ); } );

A continuación, vaya al panel de control de WordPress (cualquier página funcionará) y eche un vistazo a la consola de JavaScript de su navegador. Deberías ver el siguiente texto:

 Min between 2 and 3 is 2

¡Agradable!

¿Qué pasa con MIS dependencias?

Hablemos por un segundo sobre la administración de dependencias en JavaScript/webpack/WordPress. @wordpress/scripts está configurado de tal manera que, de manera predeterminada, si su proyecto usa una dependencia que está empaquetada en el núcleo de WordPress, aparecerá como tal en el archivo .asset.php . Esto, por ejemplo, explica por qué @wordpress/i18n se incluyó en el archivo de dependencias de nuestro script.

Pero, ¿qué pasa con las dependencias a "otros" paquetes? ¿Qué pasó con nuestro paquete utils ? Para resumir: por defecto, el paquete web compila y fusiona todas las dependencias en el script de salida. Solo mire el archivo JS generado (compile con npm run start para deshabilitar la minificación):

 ... var __webpack_modules__ = ({ "./src/utils/index.ts": ((...) => ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; }), ...

¿Ver? Nuestro código de utils está justo ahí, incrustado en nuestro script de salida.

¿Qué hay de @wordpress/i18n ? Bueno, es solo una simple referencia a una variable global:

 ... var __webpack_modules__ = ({ "./src/utils/index.ts": ..., "@wordpress/i18n": ((module)) => { module.exports = window["wp"]["i18n"]; }) ...

Como decía, @wordpress/scripts viene con un complemento, Dependency Extraction Webpack Plugin , que "excluye" ciertas dependencias del proceso de compilación y genera código asumiendo que estarán disponibles en el ámbito global. En nuestro ejemplo, por ejemplo, podemos ver que @wordpress/i18n está en wp.i18n . Por eso, al poner en cola nuestro script, también necesitamos poner en cola sus dependencias.

Configuración personalizada para generar dos scripts separados

Con todo esto en mente, digamos que queremos lograr lo mismo con nuestro paquete utils . Es decir, no queremos que su contenido se incruste en index.js , sino que debe compilarse en su propio archivo .js y aparecer como una dependencia de index.asset.php . ¿Como hacemos eso?

Primero, debemos cambiar el nombre de la declaración de import en index.js para que parezca un paquete real. En otras palabras, en lugar de importar el script usando una ruta relativa ( ./utils ), estaría bien si pudiéramos usar un nombre como @nelio/utils . Para hacer esto, todo lo que tiene que hacer es editar el archivo package.json del proyecto para agregar una nueva dependencia en dependencies :

 { ... "dependencies": { "@nelio/utils": "./src/utils" }, "devDependencies": { "@wordpress/i18n": ..., "@wordpress/scripts": ... }, ... }

ejecute npm install para crear un enlace simbólico en node_modules que apunte a este "nuevo" paquete y, finalmente, ejecute npm init en src/utils para que, desde el punto de vista de npm, @nelio/utils sea un paquete válido.

Luego, para compilar @nelio/utils en su propio script, necesitamos editar nuestra configuración webpack.config.js y definir dos exportaciones:

  • el que ya teníamos ( ./src/index.ts )
  • otra exportación para compilar ./src/utils en un archivo diferente, exponiendo sus exportaciones en una variable global denominada, por ejemplo, nelio.utils .

En otras palabras, queremos esto:

 module.exports = [ { ...config, entry: './src/index', output: { path: __dirname + '/build', filename: 'index.js', }, }, { ...config, entry: './src/utils', output: { path: __dirname + '/build', filename: 'utils.js', library: [ 'nelio', 'utils' ], libraryTarget: 'window', }, }, ];

Vuelva a compilar el código y eche un vistazo a la carpeta ./build ; verá que ahora todos tenemos dos scripts. Eche un vistazo a ./build/utils.js y verá cómo define nelio.utils , como se esperaba:

 ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; (window.nelio = window.nelio || {}).utils = __webpack_exports__; ... ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; (window.nelio = window.nelio || {}).utils = __webpack_exports__; ...

Desafortunadamente, solo estamos a mitad de camino. Si también echa un vistazo a ./build/index.js , verá que src/utils todavía está incrustado en él... ¿no debería ser una "dependencia externa" y usar la variable global que acabamos de definir?

Configuración personalizada para crear dependencias externas

Para transformar @nelio/utils en una dependencia externa real, debemos personalizar aún más nuestro paquete web y aprovechar el complemento de extracción de dependencias que mencionamos anteriormente. Simplemente vuelva a abrir el archivo webpack.config.js y modifique la variable de config de la siguiente manera:

 const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); const config = { ...defaultConfig, module: { ...defaultConfig.module, rules: [ ...defaultConfig.module.rules, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, plugins: [ ...defaultConfig.plugins.filter( ( p ) => p.constructor.name !== 'DependencyExtractionWebpackPlugin' ), new DependencyExtractionWebpackPlugin( { requestToExternal: ( request ) => '@nelio/utils' === request ? [ 'nelio', 'utils' ] : undefined, requestToHandle: ( request ) => '@nelio/utils' === request ? 'nelio-utils' : undefined, outputFormat: 'php', } ), ], };

para que todas las referencias a @nelio/utils se traduzcan como nelio.utils en el código, y exista una dependencia con el controlador de secuencias de comandos nelio-utils . Si echamos un vistazo a las dependencias de ambos scripts, vemos lo siguiente:

 build/index.asset.php <?php return array( 'dependencies' => array('nelio-utils', 'wp-i18n')... ?> build/utils.asset.php <?php return array( 'dependencies' => array()... ?>

y si miramos en ./build/index.js confirmamos que efectivamente la @nelio/utils ahora es externa:

 ... var __webpack_modules__ = ({ "@nelio/utils": ((module)) => { module.exports = window["nelio"]["utils"]; }), "@wordpress/i18n": ((module)) => { module.exports = window["wp"]["i18n"]; }) ...

Sin embargo, hay un último problema que debemos abordar. Vaya a su navegador, actualice la página del tablero y mire la consola. No aparece nada, ¿verdad? ¿Por qué? Bueno, nelio ahora depende de nelio-utils , pero este script no está registrado en WordPress… por lo que sus dependencias no se pueden cumplir ahora mismo. Para solucionar esto, edite nelio.php y registre el nuevo script:

 add_action( 'admin_enqueue_scripts', function() { $path = untrailingslashit( plugin_dir_path( __FILE__ ) ); $url = untrailingslashit( plugin_dir_url( __FILE__ ) ); $asset = require( $path . '/build/utils.asset.php' ); wp_register_script( 'nelio-utils', $url . '/build/utils.js', $asset['dependencies'], $asset['version'] ); } );

Cómo acelerar el proceso de construcción

Si ejecutamos el proceso de compilación varias veces y promediamos el tiempo que tarda en completarse, vemos que la compilación se ejecuta en unos 10 segundos:

 > yarn run build ... ./src/index.ts + 2 modules ... webpack 5.74.0 compiled successfully in 5703 ms ... ./src/utils/index.ts ... webpack 5.74.0 compiled successfully in 5726 m Done in 10.22s.

que puede no parecer mucho pero esto es un simple proyecto de juguete y, como decía, los proyectos reales como Nelio Content o Nelio A/B Testing necesitan minutos para compilarse.

¿Por qué es “tan lento” y qué podemos hacer para acelerarlo? Por lo que pude ver, el problema radicaba en la configuración de nuestro paquete web. Cuantas más exportaciones tenga en el module.exports de su module.exports , más lento será el tiempo de compilación. Sin embargo, una sola exportación es mucho, mucho más rápida.

Refactoricemos ligeramente nuestro proyecto para usar una única exportación. En primer lugar, cree un archivo export.ts en src/utils con el siguiente contenido:

 export * as utils from './index';

Luego, edite su webpack.config.js para que tenga una sola exportación con dos entradas:

 module.exports = { ...config, entry: { index: './src/index', utils: './src/utils/export', }, output: { path: __dirname + '/dist', filename: 'js/[name].js', library: { name: 'nelio', type: 'assign-properties', }, }, };

Finalmente, construya el proyecto nuevamente:

 > yarn run build ... built modules 522 bytes [built] ./src/index.ts + 2 modules ... ./src/utils/export.ts + 1 modules ... webpack 5.74.0 compiled successfully in 4339 ms Done in 6.02s.

¡Solo tomó 6 segundos, que es casi la mitad del tiempo que solía! Bastante ordenado, ¿eh?

Resumen

TypeScript te ayudará a mejorar la calidad del código , ya que te permite verificar en tiempo de compilación que los tipos son correctos y que no hay inconsistencias. Pero como todo en la vida, las ventajas de usar TypeScript tienen un precio: compilar el código se vuelve un poco más lento.

En la publicación de hoy hemos visto que, dependiendo de la configuración de su paquete web, compilar su proyecto puede ser mucho más rápido (o más lento). El punto óptimo requiere una única exportación... y de eso hemos hablado hoy.

Espero que les haya gustado la publicación. Si es así, compártelo. Si conoce otras formas de optimizar el paquete web, dígame en la sección de comentarios a continuación. ¡Que tengas un buen día!

Imagen destacada de Saffu en Unsplash.