Webpack stats file

~5 min read

Webpack stats file

Analizar la cantidad de JavaScript que estamos añadiendo en un pull request (PR) en una aplicación Next.js utilizando el archivo de estadísticas de Webpack.


Para comenzar, es importante decir que cuando nos referimos a performance es crucial comprender que a medida que incorporamos más JavaScript en nuestras aplicaciones frontend -especialmente si utilizamos React como base- es más probable que experimentemos problemas de rendimiento. Por esta razón, considero fundamental poder ejercer cierto control sobre la cantidad de JavaScript que agregamos a medida que avanzamos con los pull requests en nuestra aplicación.


Para abordar este desafío (dado que la mayoría de las aplicaciones que emplean React como librería también utilizan Webpack como empaquetador) podemos aprovechar una herramienta muy poderosa que nos proporciona este bundle. Esta herramienta consiste en generar el archivo de estadísticas (stats file) de nuestra aplicación al crear el build. Puedes encontrar más información sobre esto en la documentación oficial de Webpack


Teniendo estas herramientas en mente, una práctica altamente recomendable antes de agregar código a nuestra rama principal es detectar qué estamos incorporando a la aplicación. Esto implica tener claridad sobre la cantidad de JavaScript, CSS, imágenes y otros recursos que estamos añadiendo.


Este archivo no se genera de manera automática al realizar el build de nuestra aplicación. En este blog exploraremos cómo podemos generar este archivo y, además, cómo evaluarlo para comprender mejor qué estamos agregando cada vez que hacemos un push de nuestro código.

1- Pre-requisitos

— Tener una aplicación que utilice Webpack. Para este ejemplo, crearemos una aplicación utilizando Next.js que, por defecto utiliza Webpack.

— Instalar el plugin webpack-stats-plugin, que nos permitirá generar los archivos de estadísticas de Webpack. En este caso, usaré webpack-stats-plugin

— Escribir un script (para este caso, en Node.js) que evalúe el archivo de estadísticas generado y nos proporcione conclusiones útiles sobre los recursos agregados a nuestra aplicación.

2- Aplicación que queremos medir

Lo primero que se debe hacer es instalar el plugin webpack-stats-plugin o cualquier otro plugin que nos permita generar los archivos de estadísticas de Webpack.


npm install -–save webpack-stats-file

Una vez instalada la librería, es necesario ajustar la configuración de Webpack para integrar el plugin. Dado que, estamos trabajando con una aplicación Next.js, esto implica realizar modificaciones en el archivo next.config.js específicamente.


next.config.js

const { StatsWriterPlugin } = require('webpack-stats-plugin')


const nextConfig = {
 …
 webpack: (config, _options) => {
   config.plugins.push(
     new StatsWriterPlugin({
       filename: '../webpack-stats-base.json',
       stats: {
         assets: true,
       }
     })
   );


   return config;
 }
};


module.exports = nextConfig;

Con la configuración establecida, al ejecutar el comando npm run build, deberíamos observar un nuevo archivo JSON en la raíz de nuestra aplicación. Este archivo llevará el nombre que especificamos en la configuración, en este caso, webpack-stats-base.json.


Nota: En el ejemplo proporcionado, dentro de las configuraciones estamos incluyendo assets: true. Sin embargo, para ampliar aún más el contenido del archivo de estadísticas, podemos incluir otros atributos como entrypoints, chunks, entre otros. Para comprender el propósito de cada atributo y su funcionamiento, te recomendamos consultar la documentación oficial de Webpack en el siguiente enlace: https://webpack.js.org/api/stats/


Lo ideal en este punto sería generar este archivo de control desde nuestra rama principal (main). Una vez que hayamos creado el CLI y esté listo para su uso, deberíamos guardar el archivo de estadísticas generado en el repositorio. Esto garantizará que siempre tengamos acceso a la información sobre los recursos agregados a nuestra aplicación en la rama principal del proyecto.

3- CLI

Para instalar commander y shelljs, se puede ejecutar el siguiente comando:


npm install --save commander shelljs

Una vez instaladas estas dependencias, se instancia el CLI utilizando commander.

Y luego, añadir el manejo de esta opción.


#! /usr/bin/env node
const { Command } = require("commander");

const program = new Command();
program
 .version("1.0.0")
 .description("An example CLI Analyze webpack bundle")
 .option("-c, --compare  [value]", "Compare both stats")
 .parse(process.argv);

const options = program.opts();

...

if (options.compare) {
 const baseStats = typeof options.compare === "string" ? options.compare : "webpack-stats-base.json";
 const baseStatsFilePath = path.join(process.cwd(), baseStats)
 compare(baseStatsFilePath, 'webpack-stats.json');
}

Falta la implementación de la función compare() que es la que se encargara de hacer toda la comparación.


Lo siguiente que debemos hacer -una vez que ejecutemos el CLI dentro de una aplicación- es instalar las dependencias y realizar la compilación (build) de la misma. Con estos dos pasos completos, se debería haber generado el nuevo archivo de estadísticas (dado que agregamos el plugin en las configuraciones de Webpack en el paso anterior). Por último, podremos comparar ambos archivos para identificar las diferencias.


A continuación, vamos a ver cómo implementar esto en código. Primero, utilizar ShellJS para ejecutar los comandos npm install y luego npm run build. Y el siguiente paso, consiste en buscar ambos archivos que se han generado y compararlos entre sí.


#! /usr/bin/env node
const { Command } = require("commander");

async function compare() {
  console.log("Installing dependencies...")
  const installWorks = shell.exec("npm install").code

  console.log("Building...")
  shell.exec("npm run build")

  if (installWorks !== 0) shell.exit(1)

  // Comparation
  const filePath = path.join(folderPath, filename);


 try {
   const newData = fs.readFileSync(filePath, "utf8");
   const { assets } = JSON.parse(newData);


   let jsSize: number = 0;


   assets.forEach(({ name, size }: { name: string; size: number }) => {
     if (name.includes(".js") && !name.includes(".json"))
       return (jsSize += size);
   });


   const prevStats = fs.readFileSync(baseStatsLocation, "utf8");
   const { assets: prevStatsAssets } = JSON.parse(prevStats);


   let prevjsSize: number = 0;


   prevStatsAssets.forEach(
     ({ name, size }: { name: string; size: number }) => {
       if (name.includes(".js") && !name.includes(".json"))
         return (prevjsSize += size);
     }
   );


   const difference = [
     {
       type: "JAVASCRIPT",
       "base size (Kb)": prevjsSize / 1000,
       "PR size (Kb)": jsSize / 1000,
       "Difference (Kb)": jsSize - prevjsSize,
     },
   ];


   console.table(difference);
  } catch (err) {
    console.error(err)
  }
}

Con esto deberíamos obtener como resultado, una tabla que compare la cantidad de JavaScript que había antes con lo que estamos sumando o restando con este Pull Request (PR).


Script console exit

Hasta este punto hemos desarrollado el CLI para ejecutarlo en cualquiera de nuestras aplicaciones, pero esto se limita únicamente a un entorno local. Podemos llevarlo un paso más allá e integrarlo directamente en nuestros PRs, lo que nos permitirá controlar cada Pull Request que se envíe a nuestra rama principal.

4- CLI desde una Github Action

Para ejecutar nuestro CLI desde un GitHub Action, primero debemos publicarlo. Una forma eficiente de hacer esto automáticamente (cada vez que actualicemos nuestro CLI) es creando un GitHub Action específicamente para esta tarea.


Para lograr esto, necesitaríamos generar un token desde nuestro perfil de npm y luego agregar ese token como una variable secreta dentro de GitHub. En este ejemplo, llamémoslo NPM_AUTH_TOKEN. Luego, podemos tener un archivo llamado publish.yml dentro de la carpeta .github/workflows en nuestro repositorio, donde configuraremos el flujo de trabajo para publicar nuestro paquete en npm.


.github/workflows/publish.yml within the CLI

name: "Publish package to npm"


on:
 push:
   branches: [ main ]


jobs:
 publish:
   runs-on: ubuntu-latest
   steps:
     - name: checkout
       uses: actions/checkout@v2
     - name: node
       uses: actions/setup-node@v2
       with:
         node-version: 16
         registry-url: https://registry.npmjs.org
     - name: publish
       run: npm publish --access public
       env:
         NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}

Ahora, simplemente debemos crear la acción en nuestra aplicación Next.js para que ejecute nuestro CLI cada vez que generemos un Pull Request contra nuestra rama principal (main).


.github/workflows/analize.yml within the application to measure

name: "Analyze webpack stats"


on:
 pull_request:
   branches: [ main ]


jobs:
 build:
   runs-on: ubuntu-latest
   strategy:
     matrix:
       node: [ 20 ]


   name: Node ${{ matrix.node }} sample
   steps:
     - uses: actions/checkout@v3
     - name: Run linting rules and tests
       uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node }}
     - run: npx @fdiazpaccot/webpack-js-difference -c
       env:
         GH_TOKEN: ${{secrets.GH_TOKEN}}
         GH_USER: ${{secrets.GH_USER}}
         GH_REPO: ${{secrets.GH_REPO}}

Al ejecutarlo en un entorno de Integración Continua (CI) ya no podríamos ver las salidas del comando console.table que realizamos directamente en la consola, sin entrar a ver la acción en sí. Por lo tanto, sería necesario para simplificar y comprender rápidamente los resultados, agregar un comentario en el Pull Request con el resultado de la acción. Para lograr esto, podemos utilizar @octokit/rest para agregar el comentario.


.github/workflows/analize.yml within the application to measure

async function addComment(values: any[]) {
  const token = process.env.GH_TOKEN;
  const user = process.env.GH_USER
  const repository = process.env.GH_REPO

  let comments = ''

  values.forEach((value: any) => {
    comments += `| **${value.type}** | ${value['base size (Kb)']} | ${value['PR size (Kb)']} | ${value['Difference (Kb)']} | \n`
  })

  if (!token) return

  const octokit = new Octokit({ auth: token });

  const eventData = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));

  const pullRequestId = eventData.pull_request.number;

  let comment = `**These are the bundle size changes in this PR.**

| Type | Base size (Kb) | PR size (Kb) | Difference (Kb) | 
| :--- | :----- | :------ | :------- |
${comments}`

  octokit.issues.createComment({
    owner: user,
    repo: repository,
    issue_number: pullRequestId,
    body: comment
  })
}

Cuando el github action finalice, podremos observar un resultado similar al siguiente:


Pull request comment example

Lo que se desarrollo hasta este punto, es solamente la base. Todo lo querramos extender esta solución, esta sujero a cuánto querramos profundizar en esta solución. Se podría analizar los chunks que cada asset consume al agregar más cantidad de JavaScript, e incluso identificar el punto de entrada exacto donde esto ocurre.


¡Espero que esta información les sea útil para evitar la inyección descontrolada de cantidades excesivas de JavaScript en sus aplicaciones!


Muchas gracias por leer :)