Uno de los objetivos que teníamos para este año 2022 era pasar nuestros dos plugins estrella (Nelio Content y Nelio A/B Testing) a TypeScript y React Hooks. Aún no hemos acabado el año pero ya podemos afirmar que este objetivo ha sido un éxito total 🥳 No obstante, el camino ha sido un poco más complicado de lo esperado… especialmente de ver que, después de introducir TypeScript, los tiempos de compilación de nuestros plugins pasaron de ser de unos segundos a algo más de dos minutos; algo estaba fallando y no sabíamos qué.
Pues bien, en la entrada de hoy me gustaría hablarte un poco de esa experiencia y qué hicimos para solucionarlo. Porque, sí, TypeScript siempre hará que el proceso de compilado sea un poco más lento (a fin de cuentas, estamos añadiendo la comprobación de tipos), pero no debería suponer un gran lastre. Y es que, spoiler alert, el problema nunca fue de TypeScript; él simplemente lo hizo obvio. Así que vamos a ello.
Proyecto de ejemplo
Para ayudarte a entender el problema con el que nos encontramos hace unas semanas y cómo lo solucionamos, lo mejor que podemos hacer es crear un ejemplo bien sencillo que sea fácil de reproducir. Así que vamos a crear un pequeño plugin de WordPress en el que la compilación de código JavaScript es lenta por culpa de una mala configuración de webpack. Por supuesto, voy a suponer que tienes un entorno de desarrollo de WordPress con todas las herramientas básicas…
Creación del plugin
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 y meterle el fichero 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:

aunque, como puedes imaginar, este no haga nada.

Nelio A/B Testing
Pruebas A/B nativas en WordPress
Usa tu editor de páginas favorito en WordPress para crear variaciones y lanza pruebas A/B con solo un par de clics. No se necesita saber nada de programación para que funcione.
Creación de código TypeScript
A continuación, vamos a centrarnos en la parte JavaScript de nuestro ejemplo, ya que es donde veremos cómo la configuración que usamos puede cambiar radicalmente el tiempo de compilado de nuestro proyecto.
Lo primero que haremos será inicializar npm en la carpeta de nuestro plugin. Ejecuta esto:
npm init
y sigue las instrucciones en pantalla. Luego, instala las dependencias:
npm add -D @wordpress/scripts @wordpress/i18n
y edita el fichero package.json
para añadir los scripts de compilación:
{
...
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
},
...
}
Después, añadiremos un fichero tsconfig.json
con la configuración que queramos usar para el compilador de TypeScript. Also así puede valer para empezar:
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"outDir": "build",
"lib": [ "es7", "dom" ]
},
"exclude": [ "node_modules" ]
}
Finalmente, vamos a crear los ficheros TypeScript de nuestro proyecto. Para que el ejemplo que estamos haciendo se parezca lo más posible al código que tenenmos en Nelio Content y Nelio A/B Testing, vamos a crear una carpeta src
en nuestro proyecto con un par de ficheros TypeScript dentro: index.ts
y utils/index.ts
.
Vamos a suponer que utils/index.ts
es un paquete con utilidades (esto es, funciones que queremos poder usar en los demás ficheros de nuestro proyecto). Por ejemplo, tenemos las funciones min
y max
que, dados dos números cualesquiera, devuelven el menor y mayor de ellos respectivamente:
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, tenemos que index.ts
, el fichero principal de nuestra aplicación. No importa mucho qué hace; lo único relevante es que utiliza (alguna) de las funciones definidas en utils/index.ts
y, por lo tanto, debería tener a este último como dependencia. Por ejemplo, vamos a suponer que simplemente escribe por consola cuál es menor número de entre dos:
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 por defecto de @wordpress/scripts
Si compilamos el proyecto tal y como está definido ahora mismo usando npm run build
, veremos que todo funciona perfectamente. Y es que @wordpress/scripts
está diseñado para funcionar con nuestro ejemplo. Esto es, si tenemos un fichero index.ts
en la carpeta src
, generará un fichero index.js
en la carpeta build
junto a un fichero de dependencias index.asset.php
:
> ls build
index.asset.php index.js
El fichero de dependencias incluye los nombres de las librerías JavaScript que usamos en nuestro proyecto y que vienen empaquetadas en WordPress. Por ejemplo, dado que nuestro index.ts
usaba el paquete @wordpress/i18n
para internacionalizar cadenas de texto, verás que en el fichero index.asset.php
aparece la dependencia wp-i18n
:
build/index.asset.php
<?php
return array(
'dependencies' => array( 'wp-i18n' ),
'version' => 'c6131c7f24df4fa803b7',
);
No obstante, en mi opinión esta configuración por defecto tiene un pequeño problema. Si introducimos un bug cualquiera en el código como, por ejemplo, llamar a la función min
con un string
en lugar de un number
:
const m = min( `${ a }`, b );
esto debería dar un error y, sin embargo, compila sin problemas.
Validando el código TypeScript durante el proceso de compilación
Para solucionar el problema anterior, basta con crear nuestra propia configuración de webpack y que le añadamos el paso de compilar el código TypeScript. Simplemente crea el siguiente fichero 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',
},
};
en el que carga la configuración de webpack por defecto que viene en el paquete @wordpress/scripts
y la extendemos añadiendo el ts-loader
a los ficheros .ts
.
Ahora sí, podemos detectar los errores durante la compilación:
> 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
que, si bien hace que el proceso sea un poco más lento (pasamos de medio segundo a varios segundos de compilación), nos permite enterarnos y corregirlos antes de subir nada a producción.
Encolado de los scripts
Corrije el error y vuelve a compilar el código. Una vez lo tengas, lo único que debes hacer es encolarlo (junto a sus dependencias) en PHP y podrás ver cómo funciona en el navegador.
Abre de nuevo el fichero principal nelio.php
y añade el siguiente código al final:
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']
);
}
);
Luego, acede al escritorio de WordPress y mira en la consola JavaScript de tu navegador. Deberías ver el siguiente texto:
Min between 2 and 3 is 2
¿Y MIS dependencias?
Hablemos un segundo de la gestión de dependencias en JavaScript. @wordpress/scripts
está configurado de tal forma que, por defecto, las dependencias a paquetes incluidos en el core de WordPress se añaden al fichero de dependencias. Es por ello que, por ejemplo, hemos visto cómo @wordpress/i18n
aparecía listado en las dependencias de nuestro script.
¿Pero qué pasa con las dependencias a «otros» paquetes? ¿Dónde se mete, por ejemplo, la dependencia a nuestro paquete utils
? Pues muy sencillo: por defecto webpack compila y combina todas las dependencias dentro del script de salida con lo que, si echas un vistazo al código JavaScript generado (te recomiendo que lo hagas con un npm run start
para que no lo minifique), verás que, efectivamente, las funciones de utils
están ahí:
...
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;
};
}),
...
mientras que las dependencias a, por ejemplo, @wordpress/i18n
son una simple referencia a la variable global:
...
var __webpack_modules__ = ({
"./src/utils/index.ts": ...,
"@wordpress/i18n": ((module)) => {
module.exports = window["wp"]["i18n"];
})
...
Como te decía, WordPress viene con un plugin, Dependency Extraction Webpack Plugin, para evitar meter engordar tus scripts metiendo dependencias que ya están en el propio WordPress. En esencia, este plugin compila tu script asumiendo que ciertas dependencias estarán en una variable global (en nuestro caso, por ejemplo, tenemos que @wordpress/i18n
está en wp.i18n
) e indica, en el fichero .asset.php
qué dependencias espera que nosotros encolemos en WordPress.
Configuración personalizada para generar dos scripts
Pues bien, teniendo todo esto en cuenta, supongamos que queremos conseguir lo mismo con nuestro paquete utils
. Es decir, no queremos que su contenido se empaquete en el fichero index.js
, sino que debería compilarse en su propio fichero .js
y aparecer como dependencia en index.asset.php
. ¿Cómo hacemos eso?
Lo primero es conseguir que el import
de nuestro paquete sea, en general, un poco más cómodo. Es decir, en lugar de importar el script con un path relativo (./utils
), sería mejor poder usar un nombre en plan @nelio/utils
. Para ello, lo único que debes hacer es editar el fichero package.json
del proyecto para añadir una nueva dependencia en dependencies
:
{
...
"dependencies": {
"@nelio/utils": "./src/utils"
},
"devDependencies": {
"@wordpress/i18n": ...,
"@wordpress/scripts": ...
},
...
}
ejecutar npm install
para que meta esta nueva dependencia @nelio/utils
en node_modules
(como enlace simbólico) y, finalmente, ejecutar el comando npm init
en la carpeta src/utils
para que, desde el punto de vista de npm, @nelio/utils
sea efectivamente un paquete válido.
Luego, para que @nelio/utils
se compile en su propio script, necesitamos editar nuestra configuración de webpack.config.js
para que en lugar de tener un único export, tenga dos:
- el que ya teníamos para compilar el fichero
./src/index.ts
- y otro para compilar el paquete
./src/utils
, dejando susexport
accesibles en la variable globalnelio.utils
.
O sea, 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',
},
},
];
Si compilas el código y echas un vistazo a la carpeta ./build
, verás que ahora tenemos todos dos scripts y, en concreto, cómo ./build/utils.js
define la variable global nelio.utils
:
...
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__;
...
Por desgracia, si volvemos a mirar el contenido de ./build/index.js
vemos que sigue embebiendo el contenido de src/utils
y que en su lista de dependencias sigue sin aparecer nuestro script…
Configuración personalizada para que nuestro paquete sea una dependencia externa
Si queremos que @nelio/utils
se convierta por fin en una dependencia externa, debemos personalizar todavía un poco más nuestro webpack y aprovechar, precisamente, el plugin de extracción de dependencias que hemos mencionado anteriormente. Simplemente, vuelve a abrir el fichero webpack.config.js
y modifica la variable config
para añadir este nuevo plugin:
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',
} ),
],
};
¡Y ya está! Ahora sí están las cosas como queríamos. 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 dependencia a @nelio/utils
es ahora externa:
...
var __webpack_modules__ = ({
"@nelio/utils": ((module)) => {
module.exports = window["nelio"]["utils"];
}),
"@wordpress/i18n": ((module)) => {
module.exports = window["wp"]["i18n"];
})
...
El único problema que nos queda por resolver ahora es que, si validamos el correcto funcionamiento de nuestro plugin mirando la consola del navegador, veremos que no aparece nada. Y es que nuestro script nelio
no se está encolando porque una de sus dependencias (nelio-utils
) no está registrada en WordPress. Esto lo podemos arreglar rápidamente editando el fichero nelio.php
:
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 compilación
Después de toda esta introducción, llega la hora de la verdad. Si ejecutamos varias veces el proceso de compilado y hacemos la media de cuánto tiempo tarda, veremos que el proyecto tarda unos 10 segundos en compilarse:
> 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.
lo cual puede no parecer mucho pero, como te decía, en proyectos reales como Nelio Content o Nelio A/B Testing, el tiempo de compilación era de minutos. Entonces, ¿por qué va «lento» y qué podemos hacer para acelerarlo?
Pues, según lo que pude comprobar, el problema estaba en nuestra configuración de webpack. Si usamos múltiples module.exports
en webpack, el tiempo de compilación crece de forma exagerada, ya que cada script se compila de forma independiente. Sin embargo, si tenemos un único export, el tiempo es mucho más rápido. Permíteme enseñarte cómo.
En primer lugar, crea un fichero export.ts
en src/utils
con el siguiente contenido:
export * as utils from './index';
Y luego edita webpack.config.js
para que tenga un único export:
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',
},
},
};
Con todo esto hecho, lanza de nuevo el proceso de compilación y verás que el tiempo medio es de unos 6 segundos:
> 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.
¡Casi, casi la mitad de tiempo! Increíble, ¿verdad?
En resumen
Usar TypeScript en tus proyectos añade un plus de calidad al código, porque permite controlar en tiempo de compilación que los tipos sean correctos y no haya incoherencias. Pero como todo en esta vida, las ventajas de usar TypeScript tienen un precio: compilar el código pasa a ser un poco más lento.
En la entrada de hoy hemos visto que, según cómo tengas configurado webpack, compilar tu proyecto puede ser muchísimo más rápido (o lento). La solución perfecta parece ser tener un único export… y hoy has aprendido cómo hacerlo.
Espero que te haya gustado la entrada. Si es así, compártela. Y si sabes otras formas de optimizar este proceso, dímelo en los comentarios. ¡Nos vemos!
Deja una respuesta