Back to "Modernisering av fronten Bygg pipeline: Från CDN till bundling"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Frontend Tailwind Webpack

Modernisering av fronten Bygg pipeline: Från CDN till bundling

Tuesday, 11 November 2025

Inledning

Under det senaste året, Jag har snubblat, experimenterat, trasiga saker, och gradvis moderniserat frontend bygga pipeline för denna blogg. Detta var inte någon masterplan utförs felfritt – det var rättegång och fel, massor av fel, och envis uthållighet drivs av en tydlig vision av vad jag ville uppnå.

Jag är inte en frontend guru eller en Webpack guide. Jag är en .NET utvecklare som blev frustrerad över begränsningarna i CDN-baserade beroenden och beslutade att det måste finnas ett bättre sätt. Denna artikel dokumenterar den röriga, iterativa resa från enkla <script> taggar som pekar på unpkg till en modern kombinationsrörledning som faktiskt fungerar.

Jag gjorde massor av misstag längs vägen (som jag ska dokumentera), spenderade timmar debugging kryptiska Webpack fel, och förmodligen ominstallerade node_modules Men ihärdighet lönade sig, och jag lärde mig oerhört mycket genom min beslutsamhet att få det här rätt.

Om du är en .NET utvecklare stirrar på Webpack konfigurationsfiler undrar vad i hela världen du har gett dig in i-den här artikeln är för dig.

Jag är en gammal web performance nöt, i början av uppringd tid den första sajten jag byggde för pengar (som inte var en porr backend i Perl ... en historia för en annan tid) var en Snow Conditions webbplats som snurrade av från en automatiserad telefonlinje.

På den tiden var oron ganska annorlunda. JavaScript var praktiskt taget okänt. Jag tror även divs var RARE (på den tiden var IE div och Netscape var layer) och du skulle vara LUCKY om dina användare hade en 56K-anslutning. Så jag lärde mig att optimera (även saker som kaklade bildbakgrunder för menyalternativ) etc...

Senare på Microsoft Jag arbetade även på en automatiserad bild sprite verktyg i Web Forms - det var awesome: det extraherade små bilder från en sida, genererade CSS och en sprite bild automatiskt. Men tyvärr, som min tid på Microsoft, var det inte att vara.

Kort sagt, det är en besatthet att jag har haft hela min karriär. Ingen gillar en långsam hemsida!

Att vara rättvisCDN är inte till sin natur onda.

Fullständigt utlämnande: Den här bloggen är avsiktligt överkonstruerad. behov Allt detta komplexitet. Men bygga det på detta sätt låt mig lära mig moderna frontend verktyg ordentligt och, avgörande, gav mig något verkligt att skriva om och undervisa. Ibland är det bästa sättet att lära sig att bygga något lite löjligt och dokumentera resan.

Problemet med CDN

Till en början var mitt tillvägagångssätt okomplicerat:

<!-- 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">

Även om detta fungerar, det har flera nackdelar som blev alltmer frustrerande:

  • Tillförlitlighetsfrågor: CDNs går ner. Jag har sett unpkg och jsdelivr har avbrott. När de är nere, din webbplats är trasig
  • Oförutsägbart lastbeteende: CDN-svar kan variera kraftigt. Ibland snabbt, ibland långsamt, ibland introducerar de rasförhållanden där biblioteken laddar i fel ordning
  • Tidsbestämning: Du har ingen kontroll över när manus laddas i förhållande till varandra. Detta orsakade mig ändlös huvudvärk med bibliotek initialisering
  • Flera HTTP-förfrågningar: Varje bibliotek kräver en separat förfrågan, även med HTTP/2
  • Ingen trädskakning: Du laddar ner hela biblioteket, även om du bara använder en bråkdel av det
  • Versionsdrift: CDNs kan tjäna olika versioner om du inte fäster dem explicit, och även fastsatta versioner kan bete sig annorlunda mellan CDN leverantörer
  • Osäkerhet i byggtid: Inget sätt att fånga inkompatibiliteter förrän runtime – ofta när en användare rapporterar det
  • Begränsad optimering: Kan inte minify över gränser eller ta bort död kod
  • Utveckling offline: Kräver internetuppkoppling, vilket är irriterande när man arbetar på tåg eller flygplan

Så jag ville ha kontroll...kontroll av exakt vad min webbplats använde och BEHÖVS att arbeta. Jag ville bunta bara vad jag behövde, säkerställa tillförlitlig lastning ordning, och optimera för prestanda. Detta innebar att gå bort från CDNs och mot en kombinationsstrategi.

De är enklare att ställa in, och med HTTP/2 och HTTP/3 multiplexing, flera begäran overhead är mycket mindre kritisk än vad det brukade vara. För många projekt, särskilt små sådana eller prototyper, CDNs är fortfarande ett helt förnuftigt val. Men för en produktion plats där jag ville kontroll, tillförlitlighet, och optimering, buntning var mer vettigt.

Den moderna kombinationsmetoden

Min nuvarande inställning buntar alla JavaScript beroenden genom Webpack och processer CSS genom PostCSS.

Varför Webpack i stället för Vite?

Innan jag dyker in i de tekniska detaljerna, bör jag tala till elefanten i rummet: varför använder jag Webpack när Vite, Rollup, esbuild, och andra moderna buntar finns?

Det ärliga svaret är enkelt: Jag kände redan Webpack..

Jag hade använt Webpack i stor utsträckning när jag undervisade min "Börja webbutveckling" kurs, och det var det verktyg jag förstod. När jag bestämde mig för att modernisera den här bloggens bygg pipeline, var jag redan inför en brant inlärningskurva-förstå trädskakning, koddelning, modulsystem, PostCSS rörledningar, och hur man integrerar allt detta med ASP.NET Core.

Det är i allmänhet en god praxis när man arbetar med projekt; begränsa det "nya" till "hanterbara". Det är på sätt och vis lättare att begränsa osäkerheten.

Att lägga till "lära sig ett helt nytt byggverktyg" på toppen av det verkade onödigt. Webpack fungerar. Det är moget. Det har utmärkt dokumentation och ett massivt ekosystem. begrepp av modern kombination snarare än idiosynkrasier av ett visst verktyg.

Är Vite snabbare? Absolut. Vites utvecklingsserver med infödda ES-moduler och esbuild-driven kombination är betydligt snabbare än Webpack. För stora projekt med hundratals moduler är skillnaden dramatisk.

Ska du använda Vite till ett nytt projekt? Förmodligen, ja. Om du börjar färska och inte har befintlig Webpack kunskap, Vite är förmodligen det bättre valet. Det är snabbare, enklare att konfigurera, och representerar den moderna metoden för frontend verktyg.

Ångrar jag att jag använde Webpack? Inte alls. Det fick mig till där jag behövde vara. Lärdomarna jag lärt mig om bundling, koddelning och optimering är överförbara till alla byggverktyg. Och ärligt talat, för den här bloggens skala, prestandaskillnaden mellan Webpack och Vite är försumbar-vi pratar millisekunder i utveckling återuppbygga tider.

Det är en viktig aspekt av hur jag bygger saker; börja med vad som är EASY och bygga från den stiftelsen.

Den bredare lärdomen här är att framsteg är bättre än perfektion. Jag kunde ha tillbringat veckor med att undersöka den "bästa" bunten, jämföra riktmärken, läsa jämförelseartiklar, och plåga över beslutet. Istället valde jag det verktyg jag visste, fick det att fungera, och gick vidare. Att pragmatism höll mig frakt snarare än ändlöst överläggning.

Kanske jag en dag kan migrera till Vite. Kanske inte. Oavsett, denna blogg har en modern, optimerad bygg pipeline som fungerar tillförlitligt – och det är det viktiga.

Paketstruktur

och package.json Nu underhåller två olika beroendegrupper. Denna struktur tog mig flera försök att få rätt – initialt, Nuget och npm är KINDA liknande men nppm är mycket mindre vänlig när man lägger till 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"
  }
}

Beroenden är bibliotek som behövs på runtime (Alpine.js, HTMX, EasyMDE, etc.), medan Avvikelser är byggverktyg (Webpack, Babel, PostCSS-processorer etc.).

Bygg skript

och package.json skript har utvecklats avsevärt:

{
  "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"
  }
}

Denna inställning separerar gäller:

  • ren: Tar bort tidigare byggresultat med hjälp av rimraf
  • Kopia:* uppgifter: Kopierar statiska CSS-filer som inte behöver behandlas (dessa är antingen 'import') för ett speciellt ändamål som att ladda mitt funky Raleway teckensnitt eller adaptiva sådana som tema byta förbättringar.
  • tw:* uppgifter: Processer Tailwind CSS genom PostCSS
  • Det är ingen fara.* uppgifter: Bundles JavaScript genom Webpack
  • Dev/watch/build: Orkesterar hela rörledningen

och npm-run-all paket möjliggör parallell körning för snabbare byggen.

Du kan ställa in npm run build att köra automatiskt under din lokala bygga men det är lite av en smärta för CI (du måste se till att det är inaktiverat 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>

Att göra det bara lägga till detta till din csproj ... Du kan även säga bara köra under felsökning " etc ... men jag tycker det rörigt. Jag föredrar bara att manuellt köra det. Så npm run watch gör just det, när någon css / js-fil ändrar det körs automatiskt bygget.

OBS: I JS-världen använder de nästan varm-reload dev körs som är ganska slick och gör att du typ hatar ASP.NET Core svaga försök.

Webbpack- inställning Djupdykning

Webpack-konfigurationen är där magin sker – och där jag tillbringade större delen av min tid med att felsöka. Detta uppstod inte helt och hållet. Det är resultatet av otaliga iterationer, Stack Overflow-sökningar och läsning genom Webpack-dokumentation vid 2am för att försöka förstå varför min konstruktion genererade 47 bitars filer.

Här är den aktuella inställningen (från webpack.config.js) med detaljerade förklaringar av vad varje del gör och varför den finns där:

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

Viktiga inställningssektioner förklarade

Koddelning och chunking

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

Den här inställningen delar automatiskt upp din kod i mindre bitar:

  • Styckningsdelar: "alla": Analyserar både synkron och asynkron import
  • minStorlek: 20000: Skapar bara bitar för moduler större än 20KB
  • MaxStorlek: 100000: Försök att dela bitar större än 100KB
  • körtidChunk: Extracts Webpack körtid till en separat fil för att förbättra långsiktig caching

I praktiken genererar detta flera filer i wwwroot/js/dist/:

  • main.js - Din ansökningstillfälle
  • runtime.js - Webpack modul lastning logik
  • [vendor].[contenthash].js - Automatiskt dela upp säljare bitar
  • Ytterligare bitar för kodsplit-rutter eller lata-lastade moduler

Babels transpilering

{
    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 transpiles modern JavaScript för att stödja äldre webbläsare:

  • Målsättningar: Stöder webbläsare med > 0,25 % marknadsandel som fortfarande upprätthålls
  • moduler: falska: Bevarar ES6-moduler för trädskakning i Webpack
  • AnvändningBiltIns: 'använd': Automatiskt inkluderar polyfills endast för funktioner du använder
  • corejs: 3: Använder core-js version 3 för polyfills

Detta innebär att jag kan skriva modernt JavaScript (async/await, valfritt kedjande, nullish sammansmältning) samtidigt som jag behåller brett webbläsarstöd.

Tillverkningsminimering

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 minifierar aggressivt produktionsbyggnader:

  • drop_ konsole: Tar bort alla console.log() uttalanden
  • godkända: 3: Körs komprimering tre gånger för maximal storleksreduktion
  • Övernivå: true: Minglar variabelnamn på toppnivå
  • ren_funcs: Tar bort specifika konsolmetoder även om de tilldelas variabler
  • Kommentarer: felaktiga: Tar bort alla kommentarer från utdata

På den här bloggen minskar detta typiskt JavaScript buntstorlekar med 60-70% jämfört med unminified kod.

PostCSS Pipeline för Tailwind

Tailwind CSS bearbetas genom PostCSS med flera plugins. Denna inställning (från postcss.config.js) är barmhärtigt enkelt jämfört med Webpack:

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

Rörledningen fungerar enligt följande:

  1. Postcss-import: Löser @import uttalanden i CSS-filer
  2. medvindsstänger: Processer Tailwind direktiv och genererar nyttoklasser
  3. Automatisk prefixer: Lägger till försäljare prefix för webbläsarkompatibilitet
  4. Cssnano Ordförande: Minimerar den slutliga CSS-utgången

Inställning av motvind

och tailwind.config.js filen anger var Tailwind ska leta efter klassnamn. content sökvägar till höger var kritiska – missa en väg och Tailwind kommer inte att generera klasser för dessa filer:

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

Huvuddrag:

  • Innehåll: Skanningar .cshtml filer för klassnamn (inklusive Razorvyer och e-postmallar)
  • säker lista: Alltid inkluderar dessa klasser även om de inte finns i skanningar
  • DarkMode: "klass": Aktiverar klassbaserad mörklägesbyte
  • theme.extend: Lägger till egna färger och avståndsvärden
  • Insticksprogram: Inkluderar Tailwind plugins och DaisyUI komponentbibliotek

Tailwind skannar dessa filer vid byggtiden och extraherar bara de verktygsklasser du faktiskt använder. Det är därför den slutliga CSS- filen är mycket mindre än hela Tailwind- biblioteket.

JavaScript- ingångspunkt

och main.js Filen är där allt kommer samman. Denna fil importerar och initierar alla beroenden, och det odlas organiskt som jag har lagt till funktioner i bloggen:

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

Detta tillvägagångssätt ger flera fördelar:

  1. Ett enda importställe: Alla beroenden laddade genom en postfil
  2. Uttryckliga versioner: Package.json låser specifika versioner
  3. Trädskakning: Webpack tar bort oanvänd kod från bibliotek
  4. Total exponering: Bibliotek tillgängliga för Razor vyer via window
  5. Anpassade moduler: Tillämpningsspecifik kod indelad i importerbara moduler

Integrering av layoutmallar

Till _Layout.cshtml, Jag refererar nu endast samlade tillgångar:

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

Anmärkning Jag anger module för att låta webbläsaren veta vilken typ av JS- fil detta är & asp-append-version en snygg liten ASP.NET tag helper som bifogar en hashed version av filens innehåll till frågesträngen; effektivt cache-busting när filen ändras.

De enda återstående CDN-beroendena är:

  • Google Logga in: Kräver extern CDN för OAuth-funktionalitet
  • Andra slag, med runt tvärsnitt, av järn eller stål, med en bredd av mer än 600 mm: Ikonteckensnitt (kan levereras, men minimal fördel)
  • Umami Analytics: Tredjepartsanalys skript - själv värd så jag får se ditt besök inte Google ??

Bygg pipelinevisualisering

Här är hur hela byggprocessen flödar från källfiler till produktionstillgångar:

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

Det ser ganska komplext men verkligen det är vad WebPack gör, det hanterar det mesta av detta själv (på ett komplicerat sätt som gillar Vite inte behöver men.. )

Förbättringar av prestanda

Återigen, inte riktigt POINT. Men det laddar som en absolut kula nu. Du kan fortfarande få problem med Cloudflare s "Rocket Loader" (som mangles sida laddar händelser något) men det är TIGHT.

Fördelar för utvecklare

Utöver prestanda förbättrar den moderna byggledningen avsevärt arbetsflödet för utveckling:

Typ Säkerhet och IntelliSense

Bundling genom Webpack möjliggör korrekt JavaScript modulupplösning, vilket innebär:

  • IntelliSense i VS-kod: Automatisk komplettering för importerade moduler
  • Hoppa till definition: Navigera direkt till bibliotekets källkod
  • Inline-dokumentation: JSDoc kommentarer från bibliotek visas i IDE

NÄR bygget bryts (med npm run watch) Jag vet INSTANT, kombinerat med (små men växande) JS-enheter tester det innebär hårdare återkoppling slingor.

Byte av varm modul

Under utveckling, npm run watch möjliggör återkoppling nära instant:

npm run watch

Detta kör Webpack i klockläge, ombyggnad endast ändrade moduler:

  • Typisk ombyggnadstid: 200–400 ms
  • Webbläsaren uppdateras automatiskt: Använda webbläsare-synkronisering eller liknande verktyg
  • Bevarat ansökningstillstånd: HMR upprätthåller applikationstillstånd under uppdateringar (där stöd ges)

Hantering av beroendeförhållanden

NPM: s låsfil (package-lock.json) säkerställer konsekventa byggen i olika miljöer:

npm ci  # Clean install from lock file

Detta garanterar samma beroendeversioner i utvecklings-, CI/CD- och produktionsmiljöer.

Dessa kallas "pinning" i JS-världen, där beroendena är satta i sten. Du kan göra utan ett paket lås men då är du beroende av pacakge författare; ofta HUNDREDS av olika tpeople inte förstöra några mindre release.s

Bygg skript

De organiserade npm-skripten gör gemensamma uppgifter enkla:

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

Vanliga mönster och lösningar

Exposing Bundled Libraries Globalt

Vissa bibliotek behöver global exponering för användning i Razor-vyer eller inline-skript:

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

Sedan in .cshtml filer:

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

Hantering av CSS från JavaScript-moduler

Vissa bibliotek inkluderar CSS som behöver importeras:

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

Webbförpackning css-loader och style-loader hantera denna import:

  • CSS utvinns under byggnation
  • Injiceras på sidan via <style> taggar eller separata CSS- filer
  • Automatiskt prefixerat och minifierat

Lazy Laddar tunga bibliotek

För bibliotek som bara behövs på specifika sidor, använd dynamisk import:

// 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 automatiskt koddelar dynamisk import i separata bitar, laddad på begäran.

Hantering av vanliga JS-moduler

Vissa äldre bibliotek använder CommonJS istället för ES6-moduler:

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

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

Webpack hanterar båda modulsystemen på ett transparent sätt och omvandlar CommonJS till ES6 där så behövs.

Felsökning av gemensamma frågor

Källkartor saknas i utvecklingen

Se till att devtool är konfigurerad i webpack.config.js:

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

Detta genererar källkartor under utveckling för enklare felsökning.

CSS klass skärande frågor med tailwind

Om Tailwind tar bort klasser du använder, kontrollera content inställning:

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

Alternativt kan du använda safelist För dynamiska klasser:

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

Modulen hittades inte fel

Om Webpack inte kan lösa en modul, kontrollera:

  1. Rätt importsökväg: Relativa vägar börjar med ./ eller ../
  2. Filtillägg: Lägg till .mjs till resolve.extensions vid behov
  3. Aliaser och ätliga blommor: Användning resolve.alias för komplexa vägar
resolve: {
    extensions: ['.js', '.mjs', '.json'],
    alias: {
        '@components': path.resolve(__dirname, 'src/js/components/'),
    }
}

Bygg upp resultatfrågor

Om byggen blir långsamma:

  1. Aktivera cachelagring: Webpack cache avsevärt påskyndar återuppbyggnadar
  2. Minska Babels räckvidd: Uteslut fler kataloger från Babel bearbetning
  3. Uppgraderingsverktyg: Nyare versioner av Webpack och Babel är snabbare
  4. Parallella byggnader: Användning thread-loader för flertrådig transpilering
// Enable Webpack caching
cache: {
    type: 'filesystem',
},

Lärdomar på den hårda vägen

Låt mig dela med mig av några av de misstag jag gjorde under denna resa, så att du kan undvika dem:

Misstag #1: Försöker samla ihop allt på en gång

Mitt första försök var att riva ut alla CDN-referenser i en gång och försöka bunta allt genom Webpack. Bygget bröt spektakulärt. Jag lärde mig att inkrementell migration är din vän – flytta ett bibliotek i taget, testa noga, sedan flytta till nästa.

Misstag nr 2: Förstå inte modultyper

Jag slösade timmar på att lista ut varför vissa bibliotek inte skulle ladda. Det visade sig, mixa CommonJS (require()) och ES6-moduler (import) utan att förstå hur Webpack hanterar dem leder till kryptiska fel. experiments: { outputModule: true } Konfigurationen var inte i min ursprungliga konfiguration, och jag kunde inte räkna ut varför mina moduler inte laddade.

Lösningen kom från att läsa igenom GitHub frågor på Webpack arkiv vid midnatt.

Misstag nummer 3: Överaggressiv koddelning

Vid ett tillfälle, min konfiguration genererade bitar för allt. minSize: 10000 (10KB) vilket innebar att Webpack skapade separata filer för små allmännyttiga funktioner. Den överkorrigerade och gick till 2MB... Sidbelastningen blev ett vattenfall av 50+ små bitars förfrågningar eller hängdes väntar på stora bitar att ladda ner. Jag lärde mig att koddelning är bra, men du behöver förnuftiga tröskelvärden. Det är därför min nuvarande inställning använder minSize: 20000 (20KB).

Misstag nummer 4: Glömmer bort .mjs Förlängning

Vissa npm-paket distribuerar ES6-moduler med .mjs Webpack kommer inte att lösa dessa som standard. Jag tillbringade en pinsamt lång tid debugging varför @mostlring I needed to add .mjstoLösning.extensioner."

En enkel lösning när man väl vet det, men det tog tid att få reda på det.

Misstag # 5: Inte Caching Webpack Bygger

Tidiga byggen tog 30-60 sekunder eftersom jag inte hade aktiverat Webpacks filsystemcache. När jag lade till cachning, sjönk ombyggnadstiderna till 2-3 sekunder. Detta var en spelväxlare för arbetsflöde för utveckling men tog mig veckor att upptäcka.

Misstag #6: Tailwind Utrensning Klasser jag faktiskt använt

Min favorit debugging erfarenhet: CSS klasser som fungerade bra i utvecklingen plötsligt försvann i produktionen. Tailwind rensade dem eftersom de genererades dynamiskt i JavaScript. Lösningen var safelist Konfiguration, men först efter att jag hade slösat timmar på att undra om jag höll på att bli galen. I OCKSÅ (t.ex. i mostlylucid.pagingtaghelper projekt) använda "dummy block" där jag förenklar min Tailwind konfiguration förändringar genom att bara ha ett dolt block.


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

När Tailwind bygger det skannar cshtml-filerna för klasser; om det inte ser dem tar det bort dem. Så att ha detta dolda block säkerställer att det ser de klasser jag behöver även om de bara används i inbäddade biblioteksvyer. JS-utvecklare skannar vanligtvis också JS/TS-filer för att tillåta användning av CSS-stilar som Tailwind annars skulle missa.


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: {...}

}

Som sagt, "Tailwind Approved" metoden är att lägga till klasserna till en safelist... men jag är en ASP.NET kille, så HTML vann.

Vad jag har rätt: Envishet

Det enda jag gjorde rätt var att vägra ge upp. Varje felmeddelande var en inlärningsmöjlighet. Varje trasig bygge lärde mig något nytt om hur dessa verktyg fungerar. Jag hade en tydlig vision – hopad, optimerad utrustning som laddar snabbt – och jag fortsatte att iterera tills jag kom dit.

Slutsatser

Migrera från CDN-baserade beroenden till en modern kombinationsrörledning har varit transformerande för denna blogg, men det var inte lätt. Prestandaförbättringarna är betydande, utvecklarupplevelsen är betydligt bättre, och jag har mycket större kontroll över optimering-men det tog månader av försök och fel att komma hit.

Nyckelutflykter:

  1. Bundling minskar nyttolastens storlek: Trädskakning och minifiering eliminera oanvänd kod
  2. Koddelning förbättrar prestandan: Webbläsare laddar bara vad som behövs
  3. Bygga verktyg för att aktivera modernt JavaScript: Babel transpilation stöder de senaste språkfunktionerna samtidigt som webbläsarens kompatibilitet bibehålls
  4. PostCSS rörledning optimerar CSS: Tailwinds JIT-kompilator och cssnano levererar små stilmallar
  5. Integration med .NET är sömlös: MSBuild mål kör automatiskt frontend byggen
  6. Envishet är bättre än perfektion: Du behöver inte vara en frontend expert – du behöver bara en tydlig vision och viljan att iterera

För ASP.NET Core utvecklare vana vid enkla CDN inkluderar, detta kommer att kännas överväldigande i början. Det är normalt. Jag kände på samma sätt. Starta små, migrera ett beroende i taget, och var inte rädd för att bryta saker i utvecklingen. Du kommer att lära dig mer från felsökning trasiga byggen än du någonsin kommer från att läsa dokumentation.

Konfigurationen jag har delat här representerar månader av iteration. Din resa kommer att vara annorlunda, och det är bra. Använd detta som en utgångspunkt, inte evangeliet. Anpassa det till dina behov, experimentera, och bli inte modfälld av misslyckanden – de är en del av processen.

Är bloggen överkonstruerad? Absolut. Kunde jag ha fortsatt att använda CDN och spenderat min tid på andra saker? Visst. Men jag skulle inte ha lärt mig hälften så mycket, och jag skulle inte ha den här artikeln att dela med dig. Ibland överengineering handlar inte om destinationen – det handlar om vad du lär dig längs vägen och att kunna undervisa andra från den erfarenheten.

Om du fortfarande förlitar dig på CDNs för din frontend beroenden, Jag uppmuntrar dig att ge bundling ett försök. Börja med ett bibliotek. Se hur det känns. Iterera därifrån. Ekosystemet har mognat avsevärt, och medan inlärningskurvan är verklig, är payoff värt det.

Ytterligare läsning

logo

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