DevTips – Como compilar seus ativos de forma eficiente

Publicados: 2022-08-25

Um dos objetivos que tínhamos para este ano era refatorar nossos dois plugins principais (Nelio Content e Nelio A/B Testing) para TypeScript e React Hooks. Bem, estamos apenas a meio ano e já podemos dizer que este objetivo foi um sucesso total No entanto, devo admitir: o caminho tem sido um pouco mais complicado do que o esperado… especialmente se considerarmos que, após a introdução do TypeScript, o nosso os tempos de compilação do plugin passaram de alguns segundos para mais de dois minutos! Algo estava errado e não sabíamos o quê.

Bem, no post de hoje eu gostaria de contar um pouco sobre essa experiência e o que fizemos para corrigi-la. Afinal, todos nós sabemos que o TypeScript sempre retardará um pouco o processo de compilação (a verificação de tipos tem um preço), mas não deve ser tanto assim! Bem, alerta de spoiler: o problema nunca foi TypeScript… foi minha configuração. O TypeScript apenas o tornou “óbvio”. Então vamos começar, certo?

Projeto Brinquedo

Para ajudá-lo a entender o problema que encontramos algumas semanas atrás e como o corrigimos, o melhor que podemos fazer é criar um exemplo muito simples para você acompanhar. Vamos construir um plugin WordPress simples que usa TypeScript e como uma configuração incorreta pode resultar em tempos de compilação extremamente lentos. Se você precisar de ajuda para começar, confira este post sobre ambientes de desenvolvimento do WordPress.

Criação de plug-ins

A primeira coisa que você deve fazer é criar uma nova pasta com o nome do seu plugin (por exemplo, nelio ) no diretório /wp-content/plugins do seu WordPress. Em seguida, adicione o arquivo principal ( nelio.php ) com o seguinte conteúdo:

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

Se você fez certo, verá que agora pode ativar o plugin no WordPress:

Captura de tela do nosso plugin de brinquedo na lista de plugins
Captura de tela do nosso plugin de brinquedo na lista de plugins.

Claro, o plugin não faz nada ainda… mas pelo menos aparece

TypeScript

Vamos adicionar algum código TypeScript! A primeira coisa que faremos é inicializar o npm em nossa pasta de plugins. Rode isto:

 npm init

e siga as instruções na tela. Em seguida, instale as dependências:

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

e edite o arquivo package.json para adicionar os scripts de compilação exigidos por @wordpress/scripts :

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

Quando o npm estiver pronto, vamos personalizar o TypeScript adicionando um arquivo tsconfig.json :

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

Finalmente, vamos escrever algum código TS. Queremos que isso seja muito simples, mas “próximo o suficiente” para o que tínhamos no Nelio A/B Testing e no Nelio Content, então crie uma pasta src em nosso projeto com alguns arquivos TypeScript dentro: index.ts e utils/index.ts

Por um lado, vamos supor que utils/index.ts seja um pacote utilitário. Ou seja, ele contém algumas funções que outros arquivos em nosso projeto podem precisar. Por exemplo, digamos que fornece as funções clássicas min e max :

 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 outro lado, vamos dar uma olhada no arquivo principal do nosso aplicativo: index.ts . Para nossos propósitos de teste, tudo o que queremos é um script simples que use nosso pacote de utilitários e uma dependência do WordPress. Algo assim:

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

@wordpress/scripts Configurações padrão

Se fôssemos construir o projeto agora usando npm run build , tudo funcionaria imediatamente. E isso é simplesmente porque @wordpress/scripts (ou seja, a ferramenta subjacente que estamos usando para construir nosso projeto) foi projetada para funcionar com uma base de código como a do nosso exemplo. Ou seja, se tivermos um arquivo index.ts na pasta src , ele gerará um arquivo index.js na pasta build junto com um arquivo de dependência index.asset.php :

 > ls build index.asset.php index.js

Por que dois arquivos? Bem, um é o arquivo JavaScript compilado (duh) e o outro é um arquivo de dependências com algumas informações úteis sobre nosso script. Em particular, ele nos diz em quais bibliotecas JavaScript, dentre as incluídas no WordPress, ele se baseia. Por exemplo, nosso index.ts depende do pacote @wordpress/i18n para internacionalizar strings, e essa é uma biblioteca incluída no WordPress, então… sim, wp-i18n aparecerá em index.asset.php :

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

Infelizmente, a configuração padrão não é perfeita, se você me perguntar. Aqui está o porquê.

Se introduzirmos um bug em seu código (por exemplo, vamos chamar a função min com uma string arg em vez de um number ):

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

isso deve desencadear um erro. Mas não. Ele compila sem problemas.

Verificações de tipo durante a compilação com TypeScript

Para resolver a “limitação” mencionada acima, só precisamos criar nosso próprio arquivo de configuração do webpack e dizer a ele para usar tsc (o compilador TypeScript) sempre que encontrar o código TS. Em outras palavras, precisamos do seguinte arquivo 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 você pode ver, ele começa carregando a configuração padrão do webpack incluída no pacote @wordpress/scripts e então estende o defaultConfig adicionando um ts-loader a todos os arquivos .ts . Mole-mole!

E agora, eis que:

 > 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 nosso projeto resulta em um erro. Viva! Claro, é um pouco mais lento, mas pelo menos temos algumas verificações de segurança antes de enviar qualquer coisa para a produção.

Enfileirando os scripts

Bem, agora que você sabe que há um problema no seu código, corrija-o e compile o plugin novamente. Funcionou tudo? Legal! Porque agora é hora de enfileirar o script e suas dependências no PHP para que possamos testá-lo em nosso navegador.

Abra nelio.php e anexe o seguinte trecho:

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

Em seguida, vá para o painel do WordPress (qualquer página fará o truque) e dê uma olhada no console JavaScript do seu navegador. Você deverá ver o seguinte texto:

 Min between 2 and 3 is 2

Agradável!

E as MINHAS dependências?

Vamos falar um pouco sobre gerenciamento de dependências em JavaScript/webpack/WordPress. @wordpress/scripts é configurado de tal forma que, por padrão, se seu projeto usa uma dependência empacotada no núcleo do WordPress, ela será listada como tal no arquivo .asset.php . Isso, por exemplo, explica porque @wordpress/i18n foi listado no arquivo de dependências do nosso script.

Mas e as dependências de “outros” pacotes? O que aconteceu com nosso pacote utils ? Para encurtar a história: por padrão, o webpack compila e mescla todas as dependências no script de saída Basta olhar para o arquivo JS gerado (compile-o com npm run start para desabilitar a minificação):

 ... 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? Nosso código utils está bem ali, embutido em nosso script de saída.

E o @wordpress/i18n ? Bem, é apenas uma simples referência a uma variável global:

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

Como eu estava dizendo, @wordpress/scripts vem com um plugin, Dependency Extraction Webpack Plugin , que “exclui” certas dependências do processo de compilação e gera código assumindo que elas estarão disponíveis no escopo global. Em nosso exemplo, por exemplo, podemos ver que @wordpress/i18n está em wp.i18n . É por isso que, ao enfileirar nosso script, também precisamos enfileirar suas dependências.

Configuração personalizada para gerar dois scripts separados

Com tudo isso em mente, digamos que queremos alcançar a mesma coisa com nosso pacote utils . Ou seja, não queremos que seu conteúdo seja incorporado em index.js , mas sim compilado em seu próprio arquivo .js e apareça como uma dependência em index.asset.php . Como fazemos isso?

Primeiro, devemos renomear a instrução de import em index.js para que pareça um pacote real. Em outras palavras, em vez de importar o script usando um caminho relativo ( ./utils ), seria bom se pudéssemos usar um nome como @nelio/utils . Para fazer isso, basta editar o arquivo package.json do projeto para adicionar uma nova dependência em dependencies :

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

execute npm install para criar um link simbólico em node_modules apontando para este “novo” pacote e, finalmente, execute npm init em src/utils para que, do ponto de vista do npm, @nelio/utils seja um pacote válido.

Então, para compilar @nelio/utils em seu próprio script, precisamos editar nossa configuração webpack.config.js e definir duas exportações:

  • o que já tínhamos ( ./src/index.ts )
  • outra exportação para compilar ./src/utils para um arquivo diferente, expondo suas exportações em uma variável global chamada, por exemplo, nelio.utils .

Em outras palavras, queremos isso:

 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', }, }, ];

Compile o código novamente e dê uma olhada na pasta ./build — você verá que agora todos nós temos dois scripts. Dê uma olhada em ./build/utils.js e você verá como ele define nelio.utils , como esperado:

 ... 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__; ...

Infelizmente, estamos apenas na metade. Se você também der uma olhada em ./build/index.js , você verá que src/utils ainda está embutido nele… não deveria ser uma “dependência externa” e usar a variável global que acabamos de definir?

Configuração personalizada para criar dependências externas

Para transformar @nelio/utils em uma dependência externa real, precisamos personalizar ainda mais nosso webpack e aproveitar o plug-in de extração de dependência que mencionamos anteriormente. Simplesmente reabra o arquivo webpack.config.js e modifique a variável de config da seguinte forma:

 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 as referências a @nelio/utils sejam traduzidas como nelio.utils no código e haja uma dependência para o manipulador de script nelio-utils . Se dermos uma olhada nas dependências de ambos os scripts, veremos o seguinte:

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

e se olharmos em ./build/index.js , confirmamos que de fato a dependência @nelio/utils agora é externa:

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

Há um último problema que precisamos resolver, no entanto. Vá para o seu navegador, atualize a página do painel e veja o console. Não aparece nada, certo? Por quê? Bem, nelio agora depende de nelio-utils , mas esses scripts não estão registrados no WordPress… então suas dependências não podem ser atendidas agora. Para corrigir isso, edite nelio.php e registre o novo 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'] ); } );

Como acelerar o processo de construção

Se executarmos o processo de compilação várias vezes e calcularmos quanto tempo leva para ser concluído, veremos que a compilação é executada em cerca de 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.

o que pode não parecer muito, mas este é um projeto de brinquedo simples e, como eu estava dizendo, projetos reais como Nelio Content ou Nelio A/B Testing precisam de minutos para compilar.

Por que é “tão lento” e o que podemos fazer para acelerá-lo? Até onde eu sabia, o problema estava na configuração do nosso webpack. Quanto mais exportações você tiver no module.exports do seu module.exports , mais lento será o tempo de compilação. No entanto, uma única exportação é muito, muito mais rápida.

Vamos refatorar um pouco nosso projeto para usar uma única exportação. Antes de tudo, crie um arquivo export.ts em src/utils com o seguinte conteúdo:

 export * as utils from './index';

Em seguida, edite seu webpack.config.js para que ele tenha uma única exportação com duas 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', }, }, };

Por fim, compile o projeto novamente:

 > 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.

Levou apenas 6 segundos, o que é quase metade do tempo que costumava levar! Bem legal, hein?

Resumo

O TypeScript ajudará você a melhorar a qualidade do código , pois permite verificar em tempo de compilação se os tipos estão corretos e se não há inconsistências. Mas como tudo na vida, as vantagens de usar o TypeScript têm um preço: compilar o código torna-se um pouco mais lento.

No post de hoje vimos que, dependendo da configuração do seu webpack, compilar seu projeto pode ser muito mais rápido (ou mais lento). O ponto ideal exige uma única exportação… e é sobre isso que falamos hoje.

Espero que tenha gostado do post. Se assim for, por favor, compartilhe. Se você conhece outras maneiras de otimizar o webpack, conte-me na seção de comentários abaixo. Tenha um bom dia!

Imagem em destaque por Saffu no Unsplash.