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.
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:
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.
Mi configuración actual agrupa todas las dependencias de JavaScript a través de Webpack y procesa CSS a través de PostCSS.
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.
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.).
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:
rimrafLos 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.
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,
}
};
};
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:
En la práctica, esto genera múltiples archivos en wwwroot/js/dist/:
main.js - Punto de entrada de su solicitudruntime.js - Lógica de carga del módulo Webpack[vendor].[contenthash].js - Separar automáticamente los trozos del vendedor{
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:
Esto significa que puedo escribir JavaScript moderno (async/await, encadenamiento opcional, coalescing nulo) mientras mantengo un amplio soporte para el navegador.
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:
console.log() declaracionesEn 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.
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:
@import declaraciones en archivos CSSLos 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:
.cshtml archivos para nombres de clase (incluyendo vistas de Razor y plantillas de correo electrónico)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.
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:
windowIn _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:
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.. )
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.
Más allá del rendimiento, la construcción moderna mejora significativamente el flujo de trabajo de desarrollo:
Agrupar a través de Webpack permite una correcta resolución del módulo JavaScript, lo que significa:
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.
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:
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
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
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>
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:
<style> etiquetas o archivos CSS separadosPara 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.
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.
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.
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)/,
}
]
Si Webpack no puede resolver un módulo, compruebe:
./ o ../.mjs a resolve.extensions si es necesarioresolve.alias para rutas complejasresolve: {
extensions: ['.js', '.mjs', '.json'],
alias: {
'@components': path.resolve(__dirname, 'src/js/components/'),
}
}
Si las construcciones se vuelven lentas:
thread-loader para transpilación multihilo// Enable Webpack caching
cache: {
type: 'filesystem',
},
Permítanme compartir algunos de los errores que cometí a lo largo de este viaje, para que puedan evitarlos:
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.
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.
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).
.mjs ExtensiónAlgunos 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?
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.
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 ú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í.
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:
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.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.