Modernizar el conducto de construcción de la interfaz: de CDNs a Bundling (Español (Spanish))

Modernizar el conducto de construcción de la interfaz: de CDNs a Bundling

Tuesday, 11 November 2025

//

30 minute read

Introducción

En el último año, he tropezado, experimentado, roto cosas, y gradualmente modernizado la tubería de construcción de frontend para este blog. Esto no fue un plan maestro ejecutado sin defectos — fue ensayo y error, un montón de error, y persistencia obstinada impulsada por una visión clara de lo que quería lograr.

No soy un gurú de frontend o un asistente Webpack. Soy un desarrollador .NET que se frustró con las limitaciones de las dependencias basadas en CDN y decidió que tenía que haber una mejor manera. Este artículo documenta el viaje desordenado, iterativo de simple <script> etiquetas que apuntan a unpkg a una tubería moderna que realmente funciona.

Cometí muchos errores en el camino (que documentaré), pasé horas depurando errores crípticos de Webpack, y probablemente reinstalé node_modules Pero la persistencia valió la pena, y aprendí una gran cantidad a través de la determinación de hacer esto bien.

Si eres un desarrollador de .NET mirando los archivos de configuración de Webpack preguntándote en qué demonios te has metido, este artículo es para ti.

Soy un viejo loco del rendimiento de la web, en los primeros días de marcar-up el primer sitio que construí por dinero (que no era un motor porno en Perl... una historia para otro tiempo) era un sitio de condiciones de nieve que saltó de una línea telefónica automatizada.

En aquellos días las preocupaciones eran bastante diferentes. JavaScript era prácticamente desconocido. Creo que incluso los divs eran RARE (en aquellos días IE era div y Netscape era layer) y usted sería LUCKY si sus usuarios tenían una conexión de 56K. Así que aprendí a optimizar (incluso cosas como fondos de imagen de azulejos para elementos de menú) etc...

Más tarde en Microsoft incluso trabajé en una herramienta de sprite de imagen automatizada en formularios web - era impresionante: extraía pequeñas imágenes de una página, generaba CSS y una imagen de sprite automáticamente. Pero por desgracia, como mi tiempo en Microsoft, no iba a ser.

En resumen, es una obsesión que he llevado toda mi carrera. ¡A nadie le gusta un sitio web lento!

Para ser justos, los CDN no son inherentemente malos.

Divulgación completa: Este blog está deliberadamente sobreingeniería. necesidad Pero construirlo de esta manera me permite aprender herramientas de frontend modernas correctamente y, crucialmente, me dio algo real sobre lo que escribir y enseñar. A veces la mejor manera de aprender es construir algo un poco ridículo y documentar el viaje.

El problema con los CDN

Inicialmente, mi enfoque fue directo:

<!-- Old CDN-based approach -->
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">

Aunque esto funciona, tiene varios inconvenientes que se volvieron cada vez más frustrantes:

  • Cuestiones de fiabilidadHe visto que unpkg y jsdelivr tienen apagones. Cuando están abajo, su sitio está roto
  • Comportamiento de carga imprevisible: Las respuestas CDN pueden variar enormemente. A veces rápido, a veces lento, a veces introducen condiciones de carrera donde las bibliotecas se cargan en el orden incorrecto
  • Cuestiones relativas al calendario: Usted no tiene control sobre cuando los scripts se cargan en relación entre sí. Esto me causó dolores de cabeza interminables con la inicialización de la biblioteca
  • Múltiples peticiones HTTP: Cada biblioteca requiere una petición separada, incluso con HTTP/2
  • Nada de sacudir árboles.: Usted descarga toda la biblioteca, incluso si sólo se utiliza una fracción de ella
  • Deriva de la versión: Los CDN pueden servir diferentes versiones a menos que los pines explícitamente, e incluso las versiones fijas pueden comportarse de manera diferente entre los proveedores de CDN
  • Incertidumbre en el tiempo de construcción: No hay forma de detectar incompatibilidades hasta el tiempo de ejecución, a menudo cuando un usuario lo informa
  • Optimización limitada: No se puede minificar a través de los límites o eliminar el código muerto
  • Desarrollo fuera de línea: Requiere conectividad a Internet, lo que es molesto cuando se trabaja en trenes o aviones

Así que quería control... control de exactamente lo que mi sitio utiliza y NECESITA para trabajar. Quería combinar sólo lo que necesitaba, garantizar un orden de carga confiable, y optimizar para el rendimiento. Esto significaba alejarse de CDNs y hacia un enfoque de agrupación.

Son más simples de configurar, y con el multiplexado HTTP/2 y HTTP/3, la sobrecarga de la petición múltiple es mucho menos crítica de lo que solía ser. Para muchos proyectos, especialmente los pequeños o prototipos, los CDNs siguen siendo una opción perfectamente razonable. Pero para un sitio de producción donde quería control, fiabilidad y optimización, el agrupamiento tenía más sentido.

El enfoque moderno de abarrotes

Mi configuración actual agrupa todas las dependencias de JavaScript a través de Webpack y procesa CSS a través de PostCSS.

¿Por qué Webpack en lugar de Vite?

Antes de sumergirme en los detalles técnicos, debo dirigirme al elefante en la habitación: ¿por qué estoy usando Webpack cuando existen Vite, Rollup, esbuild y otros paquetes modernos?

La respuesta honesta es simple: Yo ya sabía Webpack.

Había usado Webpack extensamente al enseñar mi curso de "Comenzar el Desarrollo Web", y era la herramienta que entendía.Cuando decidí modernizar la tubería de construcción de este blog, ya estaba enfrentando una curva de aprendizaje empinada: entender el agitamiento de árboles, la división de códigos, los sistemas de módulos, las tuberías PostCSS, y cómo integrar todo esto con ASP.NET Core.

Generalmente es una buena práctica cuando se trabaja en proyectos; limitar lo 'nuevo' a lo 'manejable'. Es más fácil limitar la incertidumbre.

Añadiendo "aprender una herramienta de construcción completamente nueva" encima de eso parecía innecesario. Webpack funciona. Es maduro. Tiene una excelente documentación y un ecosistema masivo. Lo más importante, podría enfocar mi energía de aprendizaje en el conceptos de la agrupación moderna en lugar de las idiosincrasias de una herramienta en particular.

¿Es Vite más rápido? Absolutamente. El servidor de desarrollo de Vite con módulos nativos de ES y paquetes de esbuild es significativamente más rápido que Webpack. Para grandes proyectos con cientos de módulos, la diferencia es dramática.

¿Deberías usar Vite para un nuevo proyecto? Probablemente, sí. Si usted está empezando fresco y no tiene conocimiento Webpack existente, Vite es probablemente la mejor opción. Es más rápido, más simple de configurar, y representa el enfoque moderno de herramientas de frontend.

¿Me arrepiento de usar Webpack? Las lecciones que aprendí acerca de agrupar, dividir código y optimizar son transferibles a cualquier herramienta de construcción. Y honestamente, para la escala de este blog, la diferencia de rendimiento entre Webpack y Vite es insignificante: estamos hablando de milisegundos en tiempos de reconstrucción del desarrollo.

Es un aspecto clave de cómo construyo las cosas; comienza con lo que es FÁCIL y construye desde esa base.

La lección más amplia aquí es que progreso supera la perfección. Podría haber pasado semanas investigando el "mejor" paquete, comparando puntos de referencia, leyendo artículos de comparación, y agonizando por la decisión. En su lugar, elegí la herramienta que conocía, lo hizo funcionar, y avanzó. Ese pragmatismo me mantuvo el envío en lugar de deliberar sin fin.

Tal vez algún día pueda migrar a Vite. Tal vez no lo haga. De cualquier manera, este blog tiene una tubería de construcción moderna y optimizada que funciona de manera fiable, y eso es lo que importa.

Estructura del paquete

Los package.json Ahora mantiene dos grupos de dependencia distintos. Esta estructura me llevó a varios intentos de hacer lo correcto—inicialmente, Nuget y npm son similares a KINDA, pero nppm es mucho menos amigable al añadir pacakges.

{
  "dependencies": {
    //NOTE - This is a pre-release of these enhancements, the 1.0.0 release is OUT NOW!
    "@mostlylucid/mermaid-enhancements": "^1.0.0-alpha0",
    "alpinejs": "^3.14.1",
    "codemirror": "5.65.13",
    "core-js": "^3.39.0",
    "easymde": "2.20.0",
    "flatpickr": "^4.6.13",
    "highlight.js": "^11.10.0",
    "highlightjs-cshtml-razor": "^2.1.1",
    "html-to-image": "^1.11.13",
    "htmx.org": "^2.0.1",
    "mermaid": "^11.0.2",
    "regenerator-runtime": "^0.14.1",
    "svg-pan-zoom": "^3.6.2"
  },
  //These are just used for build; not needed to RUN the app so we have a separate place for 'em
  "devDependencies": {
    "@babel/core": "7.26.9",
    "@babel/preset-env": "7.26.9",
    "@tailwindcss/aspect-ratio": "^0.4.2",
    "@tailwindcss/forms": "^0.5.7",
    "@tailwindcss/typography": "^0.5.12",
    "autoprefixer": "10.4.21",
    "babel-loader": "10.0.0",
    "cpx": "1.5.0",
    "css-loader": "7.1.2",
    "cssnano": "7.0.6",
    "daisyui": "^4.12.10",
    "mini-css-extract-plugin": "^2.9.4",
    "npm-run-all": "4.1.5",
    "postcss": "8.5.3",
    "postcss-cli": "11.0.1",
    "postcss-import": "^16.1.0",
    "rimraf": "6.0.1",
    "style-loader": "4.0.0",
    "tailwindcss": "3.4.17",
    "terser-webpack-plugin": "^5.3.10",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4"
  }
}

Dependencias son bibliotecas necesarias en tiempo de ejecución (Alpine.js, HTMX, EasyMDE, etc.), mientras que Dependencias son herramientas de construcción (Webpack, Babel, procesadores PostCSS, etc.).

Construir guiones

Los package.json Los scripts han evolucionado significativamente:

{
  "scripts": {
    "clean": "rimraf ./.tmp ./wwwroot/css/dist ./wwwroot/js/dist",
    "copy:static": "cpx \"src/css/{raleway,easymde-overrides}.css\" \"wwwroot/css/dist\"",
    "copy:highlight": "cpx \"src/css/highlight/*.min.css\" \"wwwroot/css/highlight\"",
    "copy:all": "npm-run-all --parallel copy:static copy:highlight",
    "copy:watch": "cpx \"src/css/{raleway,easymde-overrides}.css\" \"wwwroot/css/dist\" --watch & cpx \"src/css/highlight/*.min.css\" \"wwwroot/css/highlight\" --watch",
    "tw:dev": "postcss ./src/css/main.css -o ./wwwroot/css/dist/main.css",
    "tw:prod": "postcss ./src/css/main.css -o ./wwwroot/css/dist/main.css --no-map --verbose",
    "tw:watch": "postcss ./src/css/main.css -o ./wwwroot/css/dist/main.css --watch",
    "js:dev": "webpack --env development",
    "js:prod": "webpack --mode production",
    "js:watch": "webpack watch --mode development",
    "dev": "npm-run-all clean --parallel copy:all tw:dev js:dev",
    "watch": "npm-run-all clean copy:all --parallel copy:watch tw:watch js:watch",
    "build": "npm-run-all clean copy:all --parallel tw:prod js:prod"
  }
}

Esta configuración separa las preocupaciones:

  • limpiar: Elimina las salidas de compilación anteriores utilizando rimraf
  • copia:* tareas: Copia archivos CSS estáticos que no necesitan procesamiento (estos son 'importaciones') para un propósito especial como cargar mi fuente funky Raleway o adaptativos como mejoras de conmutación de temas.
  • tw:* tareas: Procesos Coilwind CSS a través de PostCSS
  • js:* tareas: Paquetes JavaScript a través de Webpack
  • dev/watch/build: Orquestra toda la tubería

Los npm-run-all paquete permite la ejecución paralela para construcciones más rápidas.

Puede establecer npm run build ejecutarse automáticamente durante su construcción local, pero es un poco molesto para CI (necesita asegurarse de que está deshabilitado, etc).

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <SpaRoot>ClientApp\</SpaRoot>
  </PropertyGroup>

  <!-- Run npm install only when package.json changes -->
  <Target Name="NpmInstall" Inputs="$(SpaRoot)package.json" Outputs="$(SpaRoot)node_modules" BeforeTargets="Build">
    <Message Importance="high" Text="Running npm install in $(SpaRoot)" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" />
  </Target>

  <!-- Run npm build before the .NET build -->
  <Target Name="NpmBuild" DependsOnTargets="NpmInstall" BeforeTargets="Build">
    <Message Importance="high" Text="Running npm run build in $(SpaRoot)" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
  </Target>

</Project>

Para hacerlo basta con añadir esto a su csproj ... Incluso se puede decir sólo ejecutar durante debug ' etc ... pero me parece desordenado. Prefiero sólo para ejecutar manualmente. Así que npm run watch hace justo eso, cuando cualquier archivo css / js cambia se ejecuta automáticamente la compilación.

NOTA: En el mundo JS casi utilizan carreras de dev de carga caliente que es bastante hábil y te hace odiar el débil intento de ASP.NET Core.

Configuración del Webpack Buceo profundo

La configuración Webpack es donde ocurre la magia, y donde pasé la mayor parte de mi tiempo resolviendo problemas. Esto no surgió en la existencia completamente formada. Es el resultado de innumerables iteraciones, búsquedas de Stack Overflow, y la lectura a través de la documentación Webpack a las 2am tratando de entender por qué mi compilación estaba generando 47 archivos en trozos.

Aquí está la configuración actual (de webpack.config.js) con explicaciones detalladas de lo que cada parte hace y por qué está ahí:

const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');

module.exports = (env, argv) => {
    const isProduction = argv.mode === 'production';

    return {
        mode: isProduction ? 'production' : 'development',
        entry: {
            main: './src/js/main.js',
        },
        output: {
            filename: '[name].js',
            chunkFilename: '[name].[contenthash].js',
            path: path.resolve(__dirname, 'wwwroot/js/dist'),
            publicPath: '/js/dist/',
            module: true,
            clean: true,
        },
        experiments: {
            outputModule: true,
        },
        module: {
            rules: [
                {
                    test: /\.css$/i,
                    use: ['style-loader', 'css-loader'],
                },
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                ['@babel/preset-env', {
                                    targets: '> 0.25%, not dead',
                                    modules: false,
                                    useBuiltIns: 'usage',
                                    corejs: 3,
                                }],
                            ],
                        },
                    },
                },
            ],
        },
        resolve: {
            extensions: ['.js', '.mjs'],
            alias: {
                '@mostlylucid/mermaid-enhancements$': path.resolve(__dirname, 'node_modules/@mostlylucid/mermaid-enhancements/dist/index.min.js')
            }
        },
        optimization: {
            splitChunks: {
                chunks: 'all',
                minSize: 20000,
                maxSize: 100000,
                name: false,
            },
            runtimeChunk: {
                name: 'runtime',
            },
            minimize: isProduction,
            minimizer: isProduction ? [
                new TerserPlugin({
                    terserOptions: {
                        ecma: 2020,
                        compress: {
                            drop_console: true,
                            passes: 3,
                            toplevel: true,
                            pure_funcs: ['console.info', 'console.debug'],
                        },
                        mangle: {
                            toplevel: true,
                        },
                        format: {
                            comments: false,
                        },
                    },
                    extractComments: true,
                }),
            ] : [],
        },
        devtool: isProduction ? false : 'eval-source-map',
        performance: {
            hints: isProduction ? 'warning' : false,
        }
    };
};

Sections de configuración clave explicadas

Dividir y cortar el código

optimization: {
    splitChunks: {
        chunks: 'all',
        minSize: 20000,
        maxSize: 100000,
        name: false,
    },
    runtimeChunk: {
        name: 'runtime',
    },
}

Esta configuración divide automáticamente su código en trozos más pequeños:

  • trozos: 'todas': Analiza las importaciones tanto sincrónicas como asíncronas
  • minTamaño: 20000: Sólo crea trozos para módulos de más de 20KB
  • Tamaño máximo: 100000: Intentos de dividir trozos mayores de 100KB
  • tiempo de ejecuciónChunk: Extrae tiempo de ejecución Webpack en un archivo separado para mejorar el almacenamiento en caché a largo plazo

En la práctica, esto genera múltiples archivos en wwwroot/js/dist/:

  • main.js - Punto de entrada de su solicitud
  • runtime.js - Lógica de carga del módulo Webpack
  • [vendor].[contenthash].js - Separar automáticamente los trozos del vendedor
  • Trozos adicionales para rutas de reparto de código o módulos con carga perezosa

Babel Transpilation

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                ['@babel/preset-env', {
                    targets: '> 0.25%, not dead',
                    modules: false,
                    useBuiltIns: 'usage',
                    corejs: 3,
                }],
            ],
        },
    },
}

Babel transpila JavaScript moderno para soportar navegadores antiguos:

  • objetivos: Soporta navegadores con > 0,25% de cuota de mercado que todavía se mantienen
  • módulos: falso: Conserva módulos ES6 para agitar árboles Webpack
  • UseBuiltIns: 'uso': Incluye automáticamente polifills sólo para las características que utiliza
  • Corejs: 3: Utiliza core-js versión 3 para polifills

Esto significa que puedo escribir JavaScript moderno (async/await, encadenamiento opcional, coalescing nulo) mientras mantengo un amplio soporte para el navegador.

Minificación de la producción

minimizer: isProduction ? [
    new TerserPlugin({
        terserOptions: {
            ecma: 2020,
            compress: {
                drop_console: true,
                passes: 3,
                toplevel: true,
                pure_funcs: ['console.info', 'console.debug'],
            },
            mangle: {
                toplevel: true,
            },
            format: {
                comments: false,
            },
        },
        extractComments: true,
    }),
] : []

Terser minimiza agresivamente la producción construye:

  • drop_console: Elimina todo console.log() declaraciones
  • pass: 3: Ejecuta la compresión tres veces para la reducción de tamaño máximo
  • nivel superior: true: Mangles nombres de variables de nivel superior
  • Pure_funcs: Elimina métodos de consola específicos incluso si se asignan a variables
  • comentarios: false: Tira todos los comentarios de la salida

En este blog, esto generalmente reduce el tamaño de los paquetes JavaScript en un 60-70% en comparación con el código no minificado.

Gasoducto PostCSS para viento de cola

Tailwind CSS se procesa a través de PostCSS con varios plugins. Esta configuración (desde postcss.config.js) es misericordiosamente simple en comparación con Webpack:

// postcss.config.js
module.exports = {
    plugins: {
        'postcss-import': {},
        tailwindcss: {},
        autoprefixer: {},
        cssnano: { preset: 'default' }
    }
}

La tubería funciona de la siguiente manera:

  1. postcss-importación: Resuelve @import declaraciones en archivos CSS
  2. railwindcss: Procesa directivas Coilwind y genera clases de utilidades
  3. autoprefixer: Añade prefijos del vendedor para la compatibilidad del navegador
  4. cssnano: Minifica la salida CSS final

Configuración del viento de cola

Los tailwind.config.js file especifica dónde Coilwind debe buscar nombres de clase. content paths right era crítico: falta un path y Tailwind no generará clases para esos archivos:

module.exports = {
    content: ["./Views/**/*.cshtml", "./EmailSubscription/**/*.cshtml"],
    safelist: ["dark", "light"],
    darkMode: "class",
    theme: {
        fontFamily: {
            body: ["Raleway", "sans-serif"],
        },
        extend: {
            colors: {
                "custom-light-bg": "#ffffff",
                "custom-dark-bg": "#1d232a",
                primary: "#072344",
                secondary: "#00aaa1",
                // ... more custom colours
            },
        },
    },
    plugins: [
        require("@tailwindcss/aspect-ratio"),
        require("@tailwindcss/typography"),
        require("daisyui"),
    ],
};

Características principales:

  • contenido: Exploraciones .cshtml archivos para nombres de clase (incluyendo vistas de Razor y plantillas de correo electrónico)
  • safelist: Siempre incluye estas clases incluso si no se encuentran en los escaneos
  • oscuroMode: "clase": Habilita el cambio de modo oscuro basado en clases
  • tema.extender: Añade colores personalizados y valores de espaciado
  • complementos: Incluye complementos Tailwind y biblioteca de componentes DaisyUI

Tailwind escanea estos archivos en el tiempo de construcción, extrayendo sólo las clases de utilidad que realmente utiliza. Es por esto que el archivo CSS final es mucho más pequeño que la biblioteca completa de Tailwind.

Punto de entrada de JavaScript

Los main.js archivo es donde todo se une. Este archivo importa e inicializa todas las dependencias, y se ha crecido orgánicamente como he añadido características al blog:

// src/js/main.js
import hljsRazor from "highlightjs-cshtml-razor";
import mermaid from "mermaid";
import Alpine from 'alpinejs';
import htmx from "htmx.org";
import hljs from "highlight.js";
import EasyMDE from "easymde";
import 'easymde/dist/easymde.min.css';
import flatpickr from "flatpickr";
import 'flatpickr/dist/flatpickr.min.css';

// Expose libraries globally for Razor views
window.Alpine = Alpine;
window.hljs = hljs;
window.htmx = htmx;
window.mermaid = mermaid;
window.EasyMDE = EasyMDE;
window.flatpickr = flatpickr;

// Import custom modules
import { typeahead } from "./typeahead";
import { submitTranslation, viewTranslation } from "./translations";
import { codeeditor } from "./simplemde_editor";
import { globalSetup } from "./global";
import { comments } from "./comments";

// Attach to namespace
window.mostlylucid = window.mostlylucid || {};
window.mostlylucid.typeahead = typeahead;
window.mostlylucid.comments = comments();
window.mostlylucid.translations = {
    submitTranslation: submitTranslation,
    viewTranslation: viewTranslation
};

// Initialise Alpine
Alpine.start();

Este enfoque ofrece varias ventajas:

  1. Punto único de importación: Todas las dependencias cargadas a través de un archivo de entrada
  2. Versiones explícitas: Package.json bloquea versiones específicas
  3. Agitación de árboles: Webpack elimina el código no utilizado de las bibliotecas
  4. Exposición mundial: Bibliotecas disponibles para las vistas de Razor a través de window
  5. Módulos personalizados: Código específico de la aplicación organizado en módulos importables

Integración de plantilla de diseño

In _Layout.cshtml, ahora sólo me refiero a los activos agrupados:

<head>
    <link href="/css/dist/main.css" asp-append-version="true" rel="stylesheet" />
</head>
<body>
    <!-- Content -->
    <script src="~/js/dist/main.js" type="module" asp-append-version="true"></script>
</body>

Nota I module para que el navegador sepa qué tipo de archivo JS es & asp-append-version un pequeño ayudante de etiqueta ASP.NET que añade una versión hashed del contenido del archivo a la cadena de consulta; efectivamente cache-busting cuando el archivo cambia.

Las únicas dependencias restantes de la Red son las siguientes:

  • Iniciar sesión en Google: Requiere CDN externo para la funcionalidad de OAuth
  • Boxicons: Tipo de letra del icono (puede ser incluido, pero el beneficio mínimo)
  • Umami Analytics: Guión de análisis de terceros - auto alojado por lo que llegar a ver su visita no Google ??

Construir visualización de tuberías

He aquí cómo todo el proceso de compilación fluye de archivos de origen a activos de producción:

graph TD
    A[Source Files] --> B[npm run build]
    B --> C[Clean Task]
    C --> D[rimraf wwwroot/css/dist wwwroot/js/dist]

    B --> E[Copy Task]
    E --> F[cpx: Copy static CSS files]
    F --> G[wwwroot/css/dist/raleway.css]
    F --> H[wwwroot/css/dist/easymde-overrides.css]

    B --> I[Tailwind Task]
    I --> J[PostCSS Pipeline]
    J --> K[postcss-import]
    K --> L[Tailwind CSS]
    L --> M[Autoprefixer]
    M --> N[cssnano]
    N --> O[wwwroot/css/dist/main.css]

    B --> P[Webpack Task]
    P --> Q[Entry: src/js/main.js]
    Q --> R[Babel Transpilation]
    R --> S[Module Resolution]
    S --> T[Tree Shaking]
    T --> U[Code Splitting]
    U --> V[Terser Minification]
    V --> W[wwwroot/js/dist/main.js]
    V --> X[wwwroot/js/dist/runtime.js]
    V --> Y[wwwroot/js/dist/vendor chunks]

    O --> Z[ASP.NET Core Static Files]
    W --> Z
    X --> Z
    Y --> Z
    G --> Z
    H --> Z

    Z --> AA[Browser]

    style A stroke:#0ea5e9,stroke-width:3px
    style Z stroke:#f59e0b,stroke-width:3px
    style AA stroke:#10b981,stroke-width:3px

Parece bastante complejo, pero realmente eso es lo que hace WebPack, se encarga de la mayor parte de esto en sí mismo (de una manera complicada que los gustos de Vite no necesitan pero.. )

Mejoras del rendimiento

Una vez más, no es realmente el punto. Pero se carga como una bala absoluta ahora. Todavía puede tener problemas con Cloudflare 'Rocket Loader' (que mangles página cargar eventos algo) pero es TIGHT.

Beneficios de la experiencia del desarrollador

Más allá del rendimiento, la construcción moderna mejora significativamente el flujo de trabajo de desarrollo:

Seguridad de tipo e IntelliSense

Agrupar a través de Webpack permite una correcta resolución del módulo JavaScript, lo que significa:

  • IntelliSense en código VS: Autocompletado para módulos importados
  • Saltar a la definición: Navegue directamente al código fuente de la biblioteca
  • Documentación en línea: Los comentarios de JSDoc de las bibliotecas aparecen en IDE

CUANDO la construcción se rompe (con npm run watch) Conozco INSTANTLY, combinado con las (pocas pero crecientes) pruebas de unidades JS que significa bucles de retroalimentación más apretados.

Reemplazo del módulo caliente

Durante el desarrollo, npm run watch permite la retroalimentación casi instantánea:

npm run watch

Esto ejecuta Webpack en modo reloj, reconstruyendo sólo los módulos cambiados:

  • Tiempo típico de reconstrucción: 200-400ms
  • Navegador auto-refrescar: Usando la sincronización del navegador o herramientas similares
  • Estado de la solicitud conservada: HMR mantiene el estado de la aplicación durante las actualizaciones (si es compatible)

Gestión de las dependencias

Archivo de bloqueo de npm (package-lock.json) garantiza la coherencia de las construcciones en todos los entornos:

npm ci  # Clean install from lock file

Esto garantiza las mismas versiones de dependencia en entornos de desarrollo, CI/CD y producción.

Estos se conocen como 'pinning' en el mundo JS, donde las dependencias se establecen en piedra. Usted puede hacer sin un bloqueo de paquete, pero luego usted está dependiendo de los autores pacakge; a menudo HUNDREDS de personas diferentes no arruinar alguna liberación menor.s

Construir guiones

Los scripts npm organizados facilitan las tareas comunes:

npm run dev      # One-time development build
npm run watch    # Continuous development builds
npm run build    # Production build with optimisations
npm run clean    # Remove build artefacts

Patrones y soluciones comunes

Exposición de bibliotecas combinadas a nivel mundial

Algunas bibliotecas necesitan exposición global para su uso en vistas de Razor o scripts en línea:

// Make library available globally
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

Luego en .cshtml archivos:

<div x-data="{ open: false }">
    <!-- Alpine.js works because it's on window -->
</div>

Manejo de CSS desde módulos JavaScript

Algunas bibliotecas incluyen CSS que necesita importar:

import EasyMDE from "easymde";
import 'easymde/dist/easymde.min.css';  // Imported CSS is processed by Webpack

Webpack's css-loader y style-loader manejar estas importaciones:

  • CSS se extrae durante la construcción
  • Inyectado en la página a través de <style> etiquetas o archivos CSS separados
  • Automáticamente prefijado y minificado

Bibliotecas pesadas de carga perezosa

Para bibliotecas sólo necesarias en páginas específicas, utilice importaciones dinámicas:

// Only load Mermaid when needed
async function initMermaid() {
    const mermaid = await import('mermaid');
    mermaid.default.initialize({ startOnLoad: true });
}

// Call when needed
if (document.querySelector('.mermaid')) {
    initMermaid();
}

Webpack automáticamente divide las importaciones dinámicas en trozos separados, cargados bajo demanda.

Tratar con módulos CommonJS

Algunas bibliotecas antiguas utilizan CommonJS en lugar de módulos ES6:

// CommonJS require syntax
const hljs = require('highlight.js');

// Or use dynamic import
import('highlight.js').then(hljs => {
    // Use hljs
});

Webpack maneja ambos sistemas de módulos de forma transparente, convirtiendo CommonJS a ES6 donde sea necesario.

Solución de problemas comunes

Mapas fuente que faltan en el desarrollo

Garantice devtool está configurado en webpack.config.js:

devtool: isProduction ? false : 'eval-source-map',

Esto genera mapas fuente en desarrollo para una depuración más fácil.

Problemas de purga de clase CSS con viento de cola

Si Tailwind elimina las clases que estás usando, revisa el content configuración:

// tailwind.config.js
module.exports = {
    content: [
        './Views/**/*.cshtml',
        './Components/**/*.cshtml',
        // Add any other paths where classes are used
    ],
}

Alternativamente, utilice la safelist para las clases dinámicas:

safelist: [
    'bg-blue-500',
    'text-red-600',
    {
        pattern: /bg-(red|green|blue)-(400|500|600)/,
    }
]

Errores no encontrados en el módulo

Si Webpack no puede resolver un módulo, compruebe:

  1. Ruta de importación correcta: Los caminos relativos comienzan con ./ o ../
  2. Extensiones de archivoAñádase: .mjs a resolve.extensions si es necesario
  3. Alias: Usar resolve.alias para rutas complejas
resolve: {
    extensions: ['.js', '.mjs', '.json'],
    alias: {
        '@components': path.resolve(__dirname, 'src/js/components/'),
    }
}

Construir problemas de rendimiento

Si las construcciones se vuelven lentas:

  1. Activar caché: La caché de Webpack acelera significativamente las reconstrucciones
  2. Reducir el alcance de Babel: Excluir más directorios del procesamiento de Babel
  3. Herramientas de actualización: Las nuevas versiones de Webpack y Babel son más rápidas
  4. Montajes paralelos: Usar thread-loader para transpilación multihilo
// Enable Webpack caching
cache: {
    type: 'filesystem',
},

Lecciones aprendidas de la manera difícil

Permítanme compartir algunos de los errores que cometí a lo largo de este viaje, para que puedan evitarlos:

Error #1: Tratando de agrupar todo a la vez

Mi primer intento fue arrancar todas las referencias de CDN de una sola vez y tratar de incluir todo a través de Webpack. La construcción se rompió espectacularmente. Aprendí que la migración incremental es tu amigo: mover una biblioteca a la vez, probar a fondo y luego pasar a la siguiente.

Error #2: No entender los tipos de módulos

Desperdicié horas tratando de averiguar por qué ciertas bibliotecas no se cargaban. Resulta, mezclando CommonJS (require()) y módulos ES6 (import) sin entender cómo Webpack los maneja conduce a errores crípticos. experiments: { outputModule: true } La configuración no estaba en mi configuración inicial, y no pude averiguar por qué mis módulos no estaban cargando.

La solución vino de la lectura a través de GitHub problemas en el repositorio Webpack a medianoche.

Error #3: Dividir el código sobre-agresivo

En un momento dado, mi configuración estaba generando trozos para todo. minSize: 10000 (10KB) lo que significaba Webpack estaba creando archivos separados para funciones de utilidades diminutas. El sobre-corregido y fue a 2MB... La carga de página se convirtió en una cascada de más de 50 peticiones de trozos pequeños o colgado esperando enormes trozos para descargar. Aprendí que la división de código es buena, pero necesitas umbrales razonables. Es por eso que mi configuración actual utiliza minSize: 20000 (20KB).

Error #4: Olvidando el .mjs Extensión

Algunos paquetes npm distribuyen módulos ES6 con el .mjs extensión. Webpack no resolverá estos por defecto. Pasé un embarazoso largo tiempo depurando por qué @mostlring I needed to add .mjstosolution.extensions`.

Sencillo arreglo una vez que lo sepas, ¿pero descubrirlo?

Error #5: No Caching Webpack construye

Early builds tomó 30-60 segundos porque no había habilitado la caché del sistema de archivos Webpack. Una vez que añadí caché, los tiempos de reconstrucción cayeron a 2-3 segundos. Este fue un cambio de juego para el flujo de trabajo de desarrollo, pero me tomó semanas descubrirlo.

Error #6: Clases de purga de viento de cola que realmente utilicé

Mi experiencia de depuración favorita: las clases CSS que funcionaban bien en el desarrollo desaparecieron repentinamente en la producción. Tailwind las estaba purgando porque se generaban dinámicamente en JavaScript. La solución era la safelist configuración, pero sólo después de haber perdido horas preguntándome si me estaba volviendo loco. I TAMBIÉN (por ejemplo, en mostlylucid.pagingtaghelper proyecto) utilizar 'bloques tontos' donde simplifico mis cambios de configuración de Coilwind con sólo tener un bloque oculto.


<!--
    Preserve Tailwind & DaisyUI classes used in embedded pager views.
    Without this, TailwindCSS tree-shaking removes classes from the embedded library views.
-->
<span class="hidden
    btn btn-sm btn-active btn-disabled join join-item badge badge-sm select select-bordered select-sm label label-text
    px-3 py-2 py-1 text-sm font-medium border rounded whitespace-nowrap cursor-not-allowed
    text-gray-700 text-gray-600 text-gray-400 text-white bg-white bg-gray-100 bg-blue-600 border-gray-300 border-blue-600
    hover:bg-gray-50 hover:bg-blue-700
    dark:bg-gray-800 dark:bg-gray-700 dark:bg-blue-500 dark:text-gray-300 dark:text-gray-500 dark:text-gray-200
    dark:border-gray-600 dark:border-blue-500 dark:hover:bg-gray-700 dark:hover:bg-blue-600">
</span>

Cuando Tailwind construye escanea los archivos cshtml para las clases; si no los ve, los elimina. Así que tener este bloque oculto garantiza que ve las clases que necesito incluso si sólo se utilizan en vistas de biblioteca incrustadas. Los desarrolladores de JS también suelen escanear archivos JS/TS para permitir el uso de estilos CSS que de otro modo Tailwind perdería.


module.exports = {
content: ["./Views/**/*.cshtml", "./EmailSubscription/**/*.cshtml"],
-//^^^^ This is what Tailwing knows where to look for classes duritng the tree Shake. 
+//^^^^ This is what Tailwind knows where to look for classes during the tree-shake.
  
  ...

future: {...}

}

Como se mencionó, el enfoque 'Tailwind Approved' es agregar las clases a una lista segura... pero soy un tipo de ASP.NET, así que HTML ganó.

Lo que tengo bien: Persistencia

Lo único que hice bien fue negarme a rendirme. Cada mensaje de error era una oportunidad de aprendizaje. Cada construcción rota me enseñó algo nuevo sobre cómo funcionan estas herramientas. Tenía una visión clara —activos agrupados y optimizados que se cargan rápidamente— y seguí iterando hasta que llegué allí.

Conclusión

La migración de las dependencias basadas en CDN a un moderno gasoducto ha sido transformadora para este blog, pero no fue fácil. Las mejoras de rendimiento son sustanciales, la experiencia del desarrollador es significativamente mejor, y tengo mucho mayor control sobre la optimización, pero me llevó meses de prueba y error llegar hasta aquí.

Para llevar:

  1. Agrupar reduce el tamaño de la carga útil: El agitamiento de árboles y la minificación eliminan el código no utilizado
  2. La división de códigos mejora el rendimiento: Los navegadores cargan sólo lo que se necesita
  3. Construye herramientas para habilitar JavaScript moderno: Babel transpilation soporta las últimas características del lenguaje mientras mantiene la compatibilidad del navegador
  4. Gasoducto PostCSS optimiza CSS: Compilador JIT de Tailwind y cssnano entregar pequeñas hojas de estilo
  5. La integración con .NET es perfecta: MSBuild objetivos ejecutar automáticamente frontend construye
  6. La persistencia supera a la perfección: Usted no necesita ser un experto de frontend — usted sólo necesita una visión clara y la voluntad de iterar

Para los desarrolladores de ASP.NET Core acostumbrados a CDN simple incluye, esto se sentirá abrumador al principio. Eso es normal. Me sentí de la misma manera. Comience pequeño, migrar una dependencia a la vez, y no tenga miedo de romper las cosas en el desarrollo. Aprenderá más de depuración de edificios rotos que nunca lo hará de la lectura de documentación.

La configuración que he compartido aquí representa meses de iteración. Tu viaje será diferente, y eso está bien. Usa esto como punto de partida, no como evangelio. Adáptalo a tus necesidades, experimenta y no te desanimes por los fracasos —son parte del proceso.

¿Este blog es sobreingeniería? Absolutamente. ¿Podría haber seguido usando CDNs y pasar mi tiempo en otras cosas? Claro. Pero no habría aprendido la mitad, y no tendría este artículo para compartir con ustedes. A veces la sobreingeniería no se trata del destino — se trata de lo que se aprende en el camino y ser capaz de enseñar a otros de esa experiencia.

Si sigues dependiendo de los CDN para tus dependencias de frontend, te animo a que lo intentes. Empieza con una biblioteca. Mira cómo se siente. Itera desde allí. El ecosistema ha madurado significativamente, y mientras la curva de aprendizaje es real, el beneficio vale la pena.

Lectura adicional

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.