Front End Lesson 5 – HTML, CSS & JS at Scale | Dataplexa
Front End Engineering · Lesson 5

HTML, CSS & JS at Scale

Understand what breaks when a handful of components grows into thousands of files — and learn the structural conventions that keep large front end codebases from collapsing under their own weight.

The Day the Codebase Became the Problem

GitHub's engineering team documented a moment in 2019 when their CSS bundle crossed 40,000 lines. No single person understood the whole thing anymore. Engineers were afraid to delete rules — they might break something, somewhere, in a way they wouldn't catch until a user reported it. New styles got added at the bottom. Specificity wars broke out between teams. The stylesheet became a liability that everyone maintained but nobody owned.

This is not a story about incompetent engineers. GitHub has some of the best front end talent in the industry. It's a story about what happens when HTML, CSS, and JavaScript are treated as simple languages without discipline — and then the codebase scales to hundreds of contributors and thousands of files.

Every front end project starts clean. One HTML file, one CSS file, a couple of scripts. Then a feature gets added. Then ten. Then a second team joins. Three months later there are 47 CSS files, two conflicting button styles, and a JavaScript file that nobody remembers writing but everyone is afraid to touch. The technology didn't change. The scale did. And scale breaks assumptions that worked fine at small size.

HTML at Scale — Semantics as a Contract

HTML is deceptively forgiving. A browser will render a page made entirely of <div> elements. It will tolerate unclosed tags, mismatched nesting, and missing attributes. That tolerance is a feature for resilience — and a trap for teams that mistake tolerance for permission.

At scale, HTML structure is a team contract. The choices you make about which elements to use — and which attributes to include — determine how accessible the application is for screen reader users, how easily search engines index it, how robustly JavaScript can query and manipulate the DOM, and how clearly the document communicates its intent to the next engineer who reads it.

Div Soup (Anti-pattern)

Every element is a <div> or <span>. Navigation is a <div class="nav">. Buttons are <div class="btn">. Lists are nested divs. Screen readers announce nothing. Tab order is manual. JavaScript must maintain state the browser would have handled for free.

Semantic HTML (Best Practice)

Navigation lives in <nav>. Buttons are <button>. Lists are <ul>. Articles are <article>. The browser manages focus, keyboard interaction, and ARIA roles automatically. Screen readers narrate the structure correctly without a single extra attribute.

HTML at Scale — What Actually Breaks

Inconsistent heading hierarchy (<h1> inside a card that's inside an <h2> section) destroys document outline. Duplicate id attributes break anchor links and ARIA references. Missing alt text accumulates across hundreds of images as teams ship fast without auditing.

Template Discipline

Large teams enforce HTML correctness through component templates (React JSX, Vue SFCs, Svelte files) rather than hand-written HTML. The component enforces the correct structure — a <Button> component always renders a <button>, never a div. The abstraction enforces the contract.

The PixelForge Frontend team runs an automated HTML audit as part of their CI pipeline — axe-core scans every Storybook story on every pull request. When a component ships a button as a <div>, the build fails. That single gate has eliminated an entire category of accessibility regression that used to reach production weekly.

CSS at Scale — The Specificity War and How to Win It

CSS has a cascade — that's not the problem. The problem is that the cascade has no memory of intent. When two rules conflict, the browser resolves them mechanically by specificity and source order. It has no way to know that one rule was written by Team A to style cards in the dashboard, and another was written by Team B to style cards in the settings panel. They fight, one wins, and the loser's intent disappears silently.

This is the core CSS-at-scale problem: global namespace, local intent. Every CSS rule you write applies everywhere. There's no module system. No private scope. Any rule that matches a selector will style the matching element — regardless of where in the codebase that rule was written or what it was originally intended to do.

How the Industry Has Tried to Solve CSS at Scale

1
BEM (Block Element Modifier) — A naming convention, not a tool. Selectors follow the pattern .block__element--modifier. The double underscores and dashes encode the component structure into the class name itself. A button inside a card becomes .card__button--primary. The names are verbose but unambiguous. Specificity stays flat. Adopted by Yandex (where it originated), then large-scale teams across the industry throughout the 2010s.
2
CSS Modules — A build-time tool that transforms CSS class names into unique, scoped identifiers. You write .button. The build system outputs .button_a8f3d2. Two components can each have a .button class without conflict. The scope is real — enforced by tooling, not convention. React applications using Vite or webpack can adopt CSS Modules with minimal configuration.
3
CSS-in-JS (Styled Components, Emotion) — Styles written inside JavaScript files, co-located with the components they style. The library generates unique class names at runtime. The visual and behavioural code live in the same file. Props can drive styles directly: color: props.variant === 'primary' ? '#7c3aed' : '#334155'. Adopted by Airbnb, GitHub, and Atlassian at scale. Trade-off: runtime style injection adds CPU cost that shows up in Time to Interactive on slower devices.
4
Utility-First CSS (Tailwind CSS) — No component abstractions. Instead, a large vocabulary of single-purpose utility classes: flex gap-4 px-6 py-3 rounded-lg bg-violet-600 text-white font-semibold. The entire visual result is expressed directly in the HTML class list. No naming decisions. No specificity conflicts. Dead CSS is impossible because unused classes are purged at build time. Adopted by Vercel, Linear, and Tailwind Labs' own products. Trade-off: HTML becomes visually dense and the learning curve for the class vocabulary is real.

CSS Custom Properties — The Scaling Primitive Most Teams Underuse

CSS custom properties (often called CSS variables) let you define a value once and reference it everywhere: --color-primary: #7c3aed. They cascade and inherit like any other CSS value. They're live — changing one custom property via JavaScript updates every element that references it in a single style recalculation. They're the backbone of design tokens: the named values for color, spacing, typography, and elevation that keep a large codebase visually consistent without a spreadsheet of magic numbers scattered across thousands of files.

The PixelForge Design Systems team manages 94 CSS custom properties in a single tokens.css file. Color, spacing, border radii, shadow values, typography scales — all centralised. When the brand team updated the primary purple from #6d28d9 to #7c3aed, one line changed. Every button, badge, link, and border across the entire application updated in the next deploy. No grep-and-replace. No missed instances in an obscure settings page.

JavaScript at Scale — From Scripts to Modules to Architecture

JavaScript spent its first fifteen years without a module system. Developers managed dependencies by carefully ordering <script> tags in HTML — if jQuery wasn't loaded before the plugin that depended on it, the plugin crashed. Everything shared a single global namespace: window. One library named window.utils silently overwrote another. This was normal.

ES Modules — the native JavaScript module system introduced in ES2015 — changed the fundamental unit of JavaScript organisation. A module is a file. It explicitly declares what it exports. Every consumer explicitly declares what it imports. There's no shared global namespace. Dependencies are a graph that build tools can analyse, optimise, and validate. This is the foundation that made large-scale JavaScript applications buildable by teams of tens of engineers.

Approach Scope Dependency Declaration Tree-Shakeable? Used Where
Script tags Global (window) Manual ordering in HTML No Legacy codebases only
CommonJS (require()) Module-scoped require() at runtime Partial (harder) Node.js, older bundler configs
ES Modules (import/export) Module-scoped Static import declarations at the top Yes — analysed at build time All modern front end codebases

Tree shaking is the build-time process of analysing which exported values from a module are never imported anywhere, and removing them from the final bundle. It only works with ES Modules because import declarations are static — the build tool can know the full import graph without executing any code. A utility library that exports 200 functions but is imported for only 3 will contribute only those 3 functions' code to the bundle. The PixelForge Frontend team reduced their initial bundle by 38KB — from 312KB to 274KB — simply by switching a date formatting library from its CommonJS build to its ES Module build and letting Vite tree-shake the unused locale formatters.

The Bundle — Where HTML, CSS, and JS Converge at Scale

At scale, you stop thinking about individual files and start thinking about the bundle — the compiled, optimised output that the browser actually downloads. Your 2,400 JavaScript files, 300 CSS files, and 150 component HTML templates are processed by a build tool (Vite, webpack, esbuild, Rollup) and emitted as a handful of versioned, minified, compressed files that the CDN caches and the browser executes.

The bundle is the physical form of your front end at runtime. Its size determines how long users wait. Its structure determines how much of it must be downloaded before anything is interactive. Its chunking strategy determines how efficiently it caches across page navigations. Getting the bundle right is one of the highest-leverage activities in front end engineering at scale — but it's invisible work, and it requires understanding the full picture of what you're building.

Monolithic Bundle (Anti-pattern at Scale)

Everything — every page, every feature, every rarely-used admin panel — compiled into one JavaScript file. A user visiting the login page downloads code for the canvas editor, the comments system, the billing dashboard, and the settings panel. They pay for all of it before seeing anything.

PixelForge's initial bundle at launch: 1.4MB gzipped. Time to Interactive on a 4G connection: 6.2 seconds.

Code-Split Bundle (Scalable Pattern)

The application is divided into route-level chunks. The login page loads its chunk — perhaps 28KB. When the user navigates to the canvas editor, that chunk downloads — 340KB. Each chunk is cached independently. Returning users skip chunks they've already downloaded.

PixelForge after code splitting: initial chunk 84KB gzipped. Time to Interactive on the same 4G connection: 1.9 seconds.

What a Modern Bundle Actually Contains

A production Vite or webpack bundle is not just your application code. It contains: your application modules (typically 30–60% of total size), your node_modules dependencies (often 40–70% — this is where size creep hides), vendor chunks shared across routes (React, ReactDOM — typically 40–45KB gzipped together), CSS extracted from CSS-in-JS or CSS Modules, and asset references for fonts and images that are inlined below a size threshold. The vite-bundle-visualizer or webpack-bundle-analyzer makes this visible as a treemap — every senior front end engineer should run this analysis on any new codebase they join.

TypeScript — The Collaboration Layer for Large JavaScript Codebases

TypeScript is not about being strict for strictness's sake. It's about making a codebase legible to people who didn't write it. When you write a function that accepts a User object, the TypeScript type definition tells every other engineer in your team — and every IDE in existence — exactly what properties that object has, what types those properties are, and what the function returns. That information is not documentation that goes stale. It's enforced by the compiler on every build.

Stripe's engineering team has spoken publicly about TypeScript being one of the highest-leverage decisions they made as their front end scaled. Before TypeScript, a rename of an API response field required grepping the codebase and hoping you found every reference. After TypeScript, the compiler found every reference — and refused to compile until each one was updated. A class of runtime errors that had required production debugging simply stopped existing.

What TypeScript Enforces That JavaScript Cannot

1
API contract enforcement — A function typed to accept string cannot accidentally receive undefined. At scale, where dozens of engineers are calling each other's functions, this eliminates an entire class of "TypeError: Cannot read properties of undefined" runtime crashes.
2
Exhaustive union handling — When a status field can be 'loading' | 'success' | 'error' and the codebase grows and someone adds 'idle', TypeScript surfaces every switch and if statement that doesn't handle the new case. No grep required.
3
Refactoring safety — Renaming a component prop, changing a function signature, restructuring a data model — these are one-step operations in TypeScript with a good IDE. The compiler validates the change across the entire codebase instantaneously. In plain JavaScript, each refactor is a manual, error-prone process.
4
Self-documenting interfaces — A type ButtonProps definition is documentation that the compiler keeps accurate. It lists every prop, their types, which are optional, what values a discriminated union can take. The next engineer to use the component gets this information in their editor without reading a README or clicking a Storybook story.

The PixelForge codebase runs TypeScript in strict mode — strict: true in tsconfig.json. This enables noImplicitAny, strictNullChecks, and several other checks that eliminate the most dangerous class of JavaScript runtime errors before they reach a browser. Their TypeScript error count in the CI pipeline has averaged zero for eight consecutive months — not because the team doesn't make mistakes, but because the compiler catches those mistakes before the code merges.

TypeScript's Real Cost

TypeScript adds a compilation step that slows down the developer feedback loop. A large TypeScript project can take 8–15 seconds for a full type check. Incremental compilation (the default in tsc --watch mode) brings that to under a second for most changes, but cold builds remain slow. Vite sidesteps this by using esbuild for transpilation (which strips types but doesn't type-check) and running tsc --noEmit separately in CI. Fast development loop, full correctness on merge — the industry standard pattern for TypeScript-heavy front end teams.

The Three Disciplines in a Single Component

At scale, HTML, CSS, and JavaScript don't live in separate files managed by separate people with separate concerns. They live together in a component. A React component file contains JSX (HTML structure), CSS Module classes or Tailwind utilities (styles), and TypeScript logic (behaviour). The component is the unit of ownership, the unit of testing, the unit of deployment, and the unit of documentation.

This co-location is a deliberate inversion of the old "separation of concerns" principle, which used to mean "one folder for HTML, one for CSS, one for JS." That separation made sense when a page was a document. It stopped making sense when a page became an application with hundreds of interactive components. Co-locating structure, style, and behaviour inside the component means that when you delete a component, you delete everything related to it — no orphaned CSS selectors, no dangling event listeners, no HTML templates referencing styles that no longer exist.

Concern Old Pattern (File-Type Separation) Modern Pattern (Component Co-location)
HTML structure /templates/button.html — shared across unknown consumers JSX inside Button.tsx — owned by the component
CSS styles /styles/button.css — global scope, specificity risk Button.module.css — scoped, co-located, dead-code-free
JS behaviour /scripts/button.js — manual DOM queries, global listeners Hooks and handlers inside Button.tsx — scoped, lifecycle-managed
Delete the component Orphan CSS selectors remain. JS listeners may remain. HTML template exists in multiple files. Dead code accumulates. Delete Button.tsx and Button.module.css. Everything related is gone. Build confirms no remaining imports.

The PixelForge Frontend team's component structure is consistent across all 340 components in their codebase: a ComponentName.tsx file, a ComponentName.module.css file, a ComponentName.test.tsx file, and a ComponentName.stories.tsx file for Storybook documentation. Every component follows this structure without exception — not because it's enforced by a linter (though it could be), but because consistency at scale is itself a form of performance. An engineer joining the team from a different company can navigate to any component file and immediately know where everything lives.

The real test of scale discipline: Can a new engineer, on their first day, make a meaningful change to a component they've never seen — change a prop, fix a style, add a test — without asking for help? If the answer is no, the codebase has a scale problem that no individual is aware of, because everyone inside it has already absorbed the unwritten rules. Naming conventions, file structure, state management patterns, CSS methodology — these need to be written down, enforced by tooling where possible, and maintained by the team as living standards, not tribal knowledge.

Quiz

1. PixelForge's Design Systems and Frontend teams both have a .button CSS class. They're silently overriding each other in production. Which approach eliminates this conflict through tooling rather than team agreement?

2. PixelForge imports a utility library that exports 200 functions. The application uses only 3. The library ships both a CommonJS and an ES Module build. Which build should they use, and what mechanism makes it smaller?

3. PixelForge's Platform team adds a new 'idle' status to a union type used across 34 components. Twelve of those components have switch statements that don't handle 'idle', causing silent bugs in production. What TypeScript feature would have caught this before merge?

Up Next
Design Systems
PixelForge's Design Systems team builds the shared visual language that keeps 340 components consistent across five product squads — and learns where design systems actually break down in the real world.