ng'stuff

🔭 🎶 🕺

Compiler sa web app pour une livraison parfaite

Pas envie déployer des centaines de méga de dépendances ? Un build Astro, next ou autre embarque bien plus de node_modules que nécessaire.

Voici quelques techniques pour être fier de son livrable. Et si tu ne veux pas te prendre la tête, jette un œil à la dernière section ;)

Tout regrouper avec esbuild ?

On aurait pu utiliser esbuild qui est simple d’utilitation :

Bundle Astro
sh
npm exec esbuild -- \
  dist/server/entry.mjs \
  --bundle \
  --outfile=dist/server/entry.cjs \
  --external:sharp \
  --platform=node \
  --format=cjs \
  --sourcemap=inline

Esbuild reste un excellent outil, simple et rapide pour des situations plus simples. Il faut simplement savoir qu’il a ses limites dans des circonstances spécifiques comme ici en surchouche après un premier build Astro.

Tout regrouper avec rollup

Rollup est plus avancé et va nous permettre de gérer les périmètres ESM/cjs :

Bundle Astro
js
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";

export default {
  input: ["dist/server/entry.mjs"],

  external: ["sharp"],

  plugins: [
    nodeResolve({ preferBuiltins: true }),
    commonjs({ strictRequires: "auto" }),
    json(),
    typescript({ compilerOptions: { outDir: "optimized" } }),
  ],

  output: {
    dir: "optimized",
    format: "esm",
    // Preserve the commonjs boundaries, allowing deno to treat them correctly afterwards
    preserveModules: true,
    preserveModulesRoot: ".",
    // Deno doesn't allow node modules to reach _virtual (rollup helpers/wrappers) being outside node_modules
    virtualDirname: "node_modules/_virtual",
    sourcemap: "inline",
  },
};

Gros avantage : plus besoin de se prendre la tête avec les dépendances de dev ou non, transitives ou non, du type de package manager…​ On laisse rollup regrouper uniquement le nécessaire !

À condition tout de même de spécifier un peu de config :

  • preserveModules revient à préserver les frontières pour chaque module ESM/cjs — dit plus simplement : pas de bundling.
  • virtualDirname reçoit les helpers générés par rollup, pour les polyfill ou autres compatibilités. Étant donné que ces helpers sont utilisés par les versions compilées des node_modules, on préférera les placer aussi dans node_modules pour éviter aux modules de taper dehors. D’ailleurs, deno ne permet pas cela.

On se retrouve donc avec un dossier optimized en sortie qui contient l’arborescence courante en version compilée optimisée.

Utiliser `deno compile` sur son application nodejs

En principe, deno n’a pas besoin de l’étape précédente. Une application développée entièrement sous deno compilera très bien sans bidouille.

Toutefois deno compile peut aussi nous rendre service dans un environnement node.

Runtime deno en production

  • Augmente la sécurité en permettant la gestion des permissions
  • Facilite grandement le reporting télémétrique avec OTEL
  • Favorise la vitesse d’exécution par rapport à node

Un simple exécutable

  • Un déploiement plus simple
  • Moins de versions au runtime à gérer
  • Optimisé pour limiter l’IO et les risques de corruption

Appliqué à l’optimisation rollup précédente :

sh
deno compile \
  --allow-net \
  --allow-read \
  --allow-env \
  --allow-sys \
  --no-check \
  --include=optimized/dist/client \
  optimized/dist/server/entry.js

Deno se voulant secure by default, on a bien spécifié manuellement les permissions souhaitées. On peut aussi se contenter de passer le flag -A à la place pour tout autoriser comme avec node.

Les intégrations de `deno compile` font le café ☕

En fait, deno peut compiler automatiquement je cite :

  • Next.js
  • Astro
  • Fresh (1.x and 2.x)
  • Remix
  • React Router
  • SvelteKit
  • Nuxt
  • SolidStart
  • TanStack Start
  • Vite (SSR, plus SPA/MPA projects served as static output)

Pour cela il suffit donc de cibler le dossier du projet comme point d’entrée et…​ c’est tout.

sh
deno compile .

Pas de deno.json, juste un projet node existant et deno fait le nécessaire pour gérer les dépendances et lancer les étapes intermédiaires.


Les sections précédentes peuvent quand même être utiles pour gagner en contrôle. Par exemple, Astro n’optimise (vraiment) pas ses dépendances de production. Deno par souci de compatibilité embarque tout node_modules. Les perfectionnistes voudront donc passer un coup d’esbuild là dessus avant de retirer ces infâmes modules…​

sh
deno compile --bundle --minify main.ts

Je recommanderais tout de même de bien comprendre les mécanismes sous-jacents avant de s’y frotter. Et d’être prêt à utiliser une API expérimentale :)

Retour aux articles 👀