Skip to content
Kumar Chandrachooda
.NET

The Six-Megabyte File Called min.js

A 6.23 MB JavaScript bundle loaded on every page, a manifest that minifies CSS but concatenates JS, and two copies of jQuery - the first of four frontend chapters the attendance series skipped.

By Kumar Chandrachooda 06 Jul 2025 8 min read
One enormous bundle squeezing through a narrow script tag

The heaviest asset the attendance platform ever served was not an Excel export, not a profile photo, not the org chart. It was a JavaScript bundle — 6.23 MB, referenced from the site layout so that every page loaded it, and named with a .min.js extension that was, in the most literal sense available to a filename, false. Nothing in that file was minified. Nothing in any of the platform's JavaScript bundles was minified. The extension was a costume.

An Attendance Platform in ASP.NET Core spent fifteen parts on a platform I built at a mid-size IT services company — the CAS login, the stored-procedure machinery, the report builder, the dependency graveyard, the retrospective. But that series stayed, almost entirely, on the server. It went as far forward as the Razor engine — the resurrected Html.Action was its view-layer story — and stopped there. Everything in front of Razor — the JavaScript the views load, the CSS they wear, the bundles that deliver both — went unexamined. These four addendum chapters are that examination. Same platform, same register: first person, all code samples and identifiers freshly invented stand-ins, and the same working position as the parent series — the gaps between what we shipped and what we believed we shipped are the most instructive material I own.

A manifest that says min and means concat

The platform bundled its assets the classic ASP.NET Core-era way: a bundleconfig.json at the web project root, consumed at build time by BuildBundlerMinifier, one JSON object per output file. Here is the shape of it — file names invented, structure faithful:

[
  {
    "outputFileName": "wwwroot/bundles/attendance.min.css",
    "inputFiles": [
      "wwwroot/vendor/bootstrap/bootstrap.css",
      "wwwroot/css/Site.css",
      "wwwroot/css/Site2.css"
    ]
  },
  {
    "outputFileName": "wwwroot/bundles/attendance-tables.min.js",
    "inputFiles": [
      "wwwroot/vendor/jquery/jquery-3.5.1.js",
      "wwwroot/vendor/datatables/jquery.dataTables.js",
      "wwwroot/vendor/datatables/dataTables.buttons.js",
      "wwwroot/vendor/datatables/dataTables.responsive.js",
      "wwwroot/vendor/moment/moment-with-locales.js"
    ],
    "minify": { "enabled": false }
  }
]

Read the two entries against each other, because the asymmetry is the whole finding.

  • The CSS entry carries no minify key, so the tool's default applies and minification runs. The stylesheet bundle really was minified — 246 KB of output doing exactly what its extension promised.
  • The JavaScript entry carries "minify": { "enabled": false } — and so did every one of the eight JavaScript bundles in the manifest. No exceptions. For JavaScript, the tool was a concatenator: it glued the inputs together in order, wrote the result to a path ending in .min.js, and stopped.
  • The output name is only a string. Nothing anywhere validates that a file called .min.js has been minified. The convention exists for humans — cache rules, CDN configs and code reviewers all read it as a promise — and this manifest broke the promise on every JavaScript line while keeping it on every CSS line.

I cannot recover the commit that switched minification off, so what follows is reconstruction rather than record — but the pattern is a recognisable one. The minifiers of that era were temperamental; it takes exactly one vendored file with a syntax the minifier chokes on, or one plugin that misbehaves after mangling, to break a Friday build. The honest fix — exclude the one offending file, or upgrade the minifier — loses to the fast fix, which is a flag. The flag then gets copied into the next bundle entry along with everything else, because copy-paste was this platform's love language, and within a few months “minification is off for one broken file” has become “minification is off, structurally, forever”. The lie in the filename was nobody's decision; it was the residue of eight small decisions that were each somebody's five o'clock.

Six megabytes, on every page

Concatenation without minification would be a venial sin at small sizes. These were not small sizes:

Bundle Weight Loaded from
attendance-tables.min.js 6.23 MB the layout — every page
attendance-core.min.js 832 KB the layout — every page
attendance-reports.min.js 467 KB the report screens
attendance-orgchart.min.js 334 KB the org chart
attendance-report-templates.min.js 235 KB the template editor
attendance-login.min.js 216 KB the login page

The first row is the one to sit with. The table bundle — jQuery, DataTables and its extension family, and Moment with every locale it has ever supported — was referenced from _Layout.cshtml, the one file every view inherits. So the dashboard paid for it, reasonably, because the dashboard has tables. But the profile page paid for it. The virtual-timer page paid for it. Screens with no table anywhere in their markup parsed seven megabytes of table infrastructure — the two layout bundles together — before their own first line of script ran.

Let me be fair before I am unfair. This was an intranet. Most users hit the app daily, the bundles carried long cache headers, and after the first visit the bytes came from disk. On the office network even the first visit was tolerable. That is the argument that let this ship, and it is not a stupid argument.

It is, however, wrong twice. First: this platform existed substantially because of work-from-home — the virtual swipe-in timer was one of its headline features — and a WFH user on home broadband behind a VPN is not on the office network. The people the platform was built to serve were precisely the people the six-megabyte first load hurt most. Second: a cache saves you the download, not the work. Parsing and compiling six-plus megabytes of JavaScript is a real cost the browser pays on the machines the company actually issued, and it is a cost minification and splitting shrink at the source. “It's cached” answers the bandwidth question and quietly skips the compute one.

Vendored everything — and jQuery twice

Where did six megabytes of inputs come from? From a vendor tree with everything checked in: jQuery 3.5.1, Bootstrap and Popper, Moment, the DataTables family, a modal library, a toast library, a guided-tour library that shipped with six theme stylesheets (a story part three will enjoy), circular-progress plugins, an org-chart plugin, a slide-out-panel plugin, a number-animation plugin, and a particle-background library for the login page. No package manifest, no lockfile — the files themselves, committed.

And then, under wwwroot/lib, restored by LibMan: a second copy of jQuery and the validation plugins. Two package-management philosophies, one repository, and the same library twice at potentially two versions. The second copy is how a codebase acquires “which $ am I actually running” bugs, and even when it stays harmless it doubles the patching surface — a jQuery advisory now means updating two files, and the one nobody remembers is the one that stays vulnerable.

To be fair once more: vendoring in 2019, for a small team with no Node toolchain, was a defensible call. Checked-in files build offline, deploy deterministically, and never break because a registry had a bad day. The failure here is not vendoring; it is vendoring without an inventory — nobody could say what was vendored, at what version, used by which pages, because the answer lived in no manifest and the bundle flag ensured even the output files told you nothing.

What honest bundling would have cost

The reason this chapter opens the addendum is that the remedy was so cheap, and the ladder of remedies so clearly marked:

  1. Delete the flag. Remove "minify": { "enabled": false } from eight entries — or fix the one input that originally broke, if archaeology can find it. An afternoon including testing, for something like a half-to-two-thirds size reduction; that is the boring, reliable norm for minifying verbose library code.
  2. Move the table bundle out of the layout. The pages with tables declare it in their scripts section; the pages without tables stop paying a seven-megabyte tax. An hour of moving one script tag and auditing which views need it.
  3. Trim the inputs. Moment's all-locales build is famously most of Moment's weight, and this platform ran in one locale. The DataTables extensions that no view used could simply leave the bundle.
  4. Adopt a real bundler with tree-shaking and code-splitting. This is the step I would not have taken then — for a Razor-rendered intranet app, the toolchain cost outweighs the win once steps one to three are done. Honest bundling does not require modern bundling.

The cheapest performance work this platform ever declined was deleting eight lines of JSON. That is the ledger entry for this chapter: not a hard problem, not a deep trade-off — a flag that told a build tool to stop doing its job, named so that nobody would notice it had.

Where the chapters go

Four chapters, one seam each:

  1. The Six-Megabyte File Called min.js — this post.
  2. Validation Theatre — a validation framework bundled twice and wired to nothing, while the real checks are hand-stitched inside modal callbacks.
  3. CSS by Accretion — a sequel stylesheet four times the size of the original, six copied tour themes, and a modal that lives in a string.
  4. Where JavaScript Goes Without a Module System — a dead entry script, thirty-one inline script blocks, and a 2,904-line page file.

If your bundle manifest has a minify key you have never audited, or your layout loads a table library onto pages without tables, start with the flag. Next: the framework the platform shipped twice and used never — Validation Theatre.