Here's a truth that took me years to fully appreciate: how your code looks matters almost as much as what it does. Well-formatted code gets reviewed faster, has fewer bugs, and is dramatically easier to maintain. And on the flip side, properly minified code loads faster and saves your users bandwidth.
Let's talk about both sides of this coin.
Why Formatting Matters More Than You Think
I've seen teams waste hours in code reviews arguing about tabs vs spaces, semicolons vs no semicolons, or where to put curly braces. That's time not spent shipping features. Consistent formatting eliminates these debates entirely.
But it's not just about team harmony. Consistently formatted code makes bugs easier to spot. Consider this:
That sendNotification() call runs every time, not just for admins — the indentation is misleading. With mandatory braces (which most formatters enforce), this bug is obvious:
Formatting Best Practices
Pick a style and automate it. Don't rely on humans to format consistently. Use Prettier — it's opinionated, and that's the point. Set it up to run on save, and never think about formatting again.
Indentation: 2 spaces is the JavaScript/TypeScript convention. The Google style guide, Airbnb style guide, and Standard style all agree on this.
Semicolons: Use them. Yes, JavaScript has ASI (Automatic Semicolon Insertion), but it has well-documented gotchas. Just add the semicolons.
Line length: Keep lines under 80-100 characters. Long lines cause horizontal scrolling and make diffs harder to read.
How Minification Actually Works
Minification is the opposite of formatting — it makes code as small as possible for production. Here's what a minifier like Terser does:
- Removes whitespace and comments — All those spaces, tabs, newlines, and
// TODO: fix latercomments? Gone. - Shortens variable names —
userAccountBalancebecomesa.calculateMonthlyPaymentbecomesb. Local variables only — it won't rename things that external code might reference. - Eliminates dead code — If a function is never called, it gets removed.
- Pre-computes constants —
const TAX_RATE = 0.2; total * (1 + TAX_RATE)becomestotal*1.2.
The result? Typical savings of 50-70% file size reduction. Here's a before/after:
Before (readable, 147 bytes):
After (minified, 62 bytes):
Same functionality, 58% smaller.
Don't Forget Source Maps
Minified code is unreadable, which makes debugging production errors painful. Source maps solve this by mapping minified code back to your original source. Every modern build tool generates them — make sure you're uploading them to your error tracking service (Sentry, Bugsnag, etc.).
The Practical Workflow
The Practical Workflow
In development: write formatted, readable code. Use Prettier + ESLint for automatic formatting and linting. In production: minify everything through your build tool (webpack, Vite, esbuild).
Setting Up Prettier + ESLint in 60 Seconds
If you haven't automated formatting yet, here's the quickest setup:
Then add a format script to your package.json: "format": "prettier --write src/**/*.{js,ts,jsx,tsx}". Run it once to format your entire codebase, and set your editor to format on save going forward. The initial diff might be huge, but after that, formatting debates are over forever.
Tree Shaking vs Minification
People often confuse these two, but they're different optimization techniques that work together:
- Minification makes individual files smaller by removing whitespace, shortening names, and pre-computing expressions. It doesn't remove unused exports.
- Tree shaking eliminates unused code across modules. If you import only
{ debounce }from lodash-es, tree shaking removes everything else from the bundle.
Both are essential for production builds. Modern bundlers like Vite and esbuild do both automatically — just make sure you're using ES module imports (import) rather than CommonJS (require) so tree shaking can analyze your dependency graph.
Common Minification Pitfalls
1. Relying on function.name in production. Minifiers rename functions, so myFunction.name will return "a" or something equally useless in production. If you need stable function names (for logging, error tracking, or reflection), use explicit string identifiers instead.
2. Minifying code that uses eval(). eval() can reference any variable by name, so the minifier can't safely rename anything in that scope. Most minifiers will skip renaming in functions that contain eval(), but this limits optimization significantly. Avoid eval() entirely if possible.
3. Not testing minified builds. Some bugs only appear after minification — especially around property name mangling. Always run your test suite against the production build, not just the development build.
Measuring Your Bundle Size
Minification only matters if you know what you're starting with. Here are quick ways to check your bundle size:
These tools generate visual treemaps showing exactly which dependencies are eating up your bundle. You might be surprised — I once found a project where moment.js locales accounted for 500KB of the final bundle. Switching to dayjs saved 490KB instantly.
Prettier vs ESLint: They're Not the Same Thing
Okay, I need to get this off my chest because I see this confusion *everywhere*: Prettier and ESLint are not the same tool. They're not even doing the same job. And yet, I constantly see devs treating them like they're interchangeable. Let me break it down.
Prettier is a code formatter. It cares about how your code *looks* — whitespace, line breaks, semicolons, trailing commas, quote style, indentation. That's it. It doesn't know or care whether your code actually works. You could have a function that deletes your entire database and Prettier would just make sure it's nicely indented.
ESLint, on the other hand, is a linter. It cares about code *quality* — unused variables, unreachable code, potential bugs, missing error handling, accessibility issues. It catches the stuff that'll bite you at 2 AM on a Saturday when you're getting paged.
Here's the thing though — ESLint *also* has some formatting rules built in, and that's where things get messy. If you run both tools without configuring them properly, they'll fight each other. Prettier formats your code one way, ESLint complains it should be another way, you fix it for ESLint, Prettier reformats it... it's an infinite loop of frustration.
The fix is dead simple: use eslint-config-prettier. It turns off all the ESLint rules that conflict with Prettier, so ESLint focuses on code quality and Prettier handles formatting. No more fights.
Notice how "prettier" is the last item in the extends array? That's crucial — it needs to override any formatting rules from plugins listed above it. I've seen teams spend hours debugging ESLint/Prettier conflicts only to discover they had the order wrong. Trust me on this one.
My recommended setup: let Prettier handle everything cosmetic, and configure ESLint rules that actually catch bugs. Don't waste ESLint on complaining about semicolons when Prettier already handles that.
The Great Tabs vs Spaces Debate (Settled)
Alright, let's talk about the holy war of programming. Tabs or spaces? I've watched friendships end over this debate. I've seen Slack threads go on for *days*. I've personally witnessed a senior developer write a 2,000-word Confluence page defending tabs. It was magnificent and completely unnecessary.
Let's look at the actual data. The Stack Overflow Developer Survey has consistently shown that spaces are more popular — roughly 60-65% of developers prefer spaces. GitHub's own analysis of public repos tells a similar story.
But here's what's interesting: it varies wildly by language. Go uses tabs — that's not even a debate, gofmt enforces tabs and nobody argues with gofmt. Python uses 4 spaces — PEP 8 says so, and you don't argue with PEP 8 either. JavaScript and TypeScript? The community has largely settled on 2 spaces, and pretty much every major style guide agrees.
The accessibility argument for tabs is actually compelling though — tabs let each developer set their preferred visual width, which matters for developers with visual impairments. That's a real, legitimate reason to prefer tabs.
But you know what? Here's the *actual* correct answer, and I mean this sincerely: use whatever your formatter enforces and stop arguing about it. If your project uses Prettier with tabWidth: 2 and useTabs: false, then you use 2 spaces. Period. The formatter makes the decision, you accept it, and you spend your energy on things that actually matter — like whether your app crashes when someone enters an emoji in the search box.
Life is too short for formatting arguments. Let the robots decide.
Modern Minification: esbuild, SWC, and the Speed Revolution
For years, Terser was the king of JavaScript minification. It replaced UglifyJS, it was battle-tested, it worked great. The only problem? It's slow. Like, *really* slow on large codebases. And I don't mean "grab a coffee" slow — I mean "reconsider your career choices while watching the CI pipeline" slow.
Then esbuild showed up and changed everything. Written in Go, esbuild is 10-100x faster than Terser. I'm not exaggerating — the benchmarks are almost comical. A project that takes Terser 30 seconds to minify? esbuild does it in 300 milliseconds. The first time I saw it, I genuinely thought something was broken because it finished so fast.
SWC is another contender, written in Rust. It's not quite as fast as esbuild for pure minification, but it's a more complete toolchain — it handles transpilation, bundling, and minification all in one. If you're using Next.js, you're already using SWC under the hood.
Here's a rough comparison on a medium-sized project (~500 JS files):
| Tool | Language | Minification Time | Notes |
| Terser | JavaScript | ~25s | Battle-tested, most compatible |
| esbuild | Go | ~0.3s | Blazing fast, some edge cases |
| SWC | Rust | ~0.8s | Full toolchain, great ecosystem |
Now, does the speed difference actually matter? Honestly, for a small project with a 5-second build, probably not. But when you're running CI/CD on a large monorepo and your build pipeline runs 50 times a day? Those minutes add up fast. I've seen teams shave 10+ minutes off their CI times just by switching from Terser to esbuild.
The good news is that if you're using Vite, you're already getting esbuild for development builds and Rollup (with Terser or esbuild) for production. Next.js uses SWC. Angular has been experimenting with esbuild too. The ecosystem is moving fast here.
One word of caution: esbuild and SWC don't always produce byte-for-byte identical output to Terser. In rare cases, Terser's more aggressive optimizations produce slightly smaller bundles. But we're talking about maybe 1-2% difference — totally worth the 100x speed improvement in most cases.
CSS and HTML Minification Too
We've been talking about JavaScript, but don't sleep on CSS and HTML minification. Seriously, I've seen projects where the CSS bundle was *bigger* than the JavaScript. Especially if you're using a utility-first framework like Tailwind (before PurgeCSS does its thing).
For CSS, cssnano is the go-to tool. It does more than just strip whitespace — it also merges duplicate rules, converts colors to shorter formats (#ff0000 becomes #f00 or even red), removes redundant properties, and optimizes calc() expressions. On a typical stylesheet, you can expect 30-50% savings.
For HTML, html-minifier-terser strips whitespace between tags, removes optional closing tags, minifies inline CSS and JS, removes HTML comments, and collapses boolean attributes. It's surprisingly effective — I've seen 20-30% reduction on HTML-heavy pages.
Now here's the good part: if you're using Angular, React, Vue, or pretty much any modern framework with a build step, this is already happening for you. Your build tool handles CSS and HTML minification as part of the production build. But knowing what's happening under the hood is genuinely useful — especially when you need to debug why your production HTML doesn't match your source, or when you're trying to optimize that last critical render path.
Code Formatting in CI/CD Pipelines
Look, here's the thing about "format on save" — it only works if *every single person on the team* has it configured correctly. And I've been burned by this before. You know the scenario: someone joins the team, clones the repo, starts making changes, and submits a PR with 400 formatting changes mixed in with their 5 lines of actual code. Reviewing that PR is a nightmare.
The solution? Enforce formatting in CI. Make it impossible to merge unformatted code. Here's how:
Step 1: Add a CI check. Add prettier --check . to your CI pipeline. It exits with code 1 if any file doesn't match Prettier's formatting. No arguments, no debates — the CI is the law.
Step 2: Add pre-commit hooks. Catching formatting issues in CI is great, but it's even better to catch them *before* the code is even committed. That's where Husky and lint-staged come in.
The beauty of lint-staged is that it only runs on the files you've actually changed, not the entire codebase. So the pre-commit hook takes 1-2 seconds instead of 30. Nobody's going to disable a hook that takes 1 second.
I've seen teams go from "formatting is a constant source of PR friction" to "we literally never think about formatting anymore" within a week of setting this up. It's one of those things where the 15-minute setup pays for itself a thousand times over.
Real-World Bundle Size Case Studies
Let's talk real numbers, because abstract advice about "keeping bundles small" isn't very helpful without context. I've seen these exact scenarios play out in production projects, and the differences are genuinely shocking.
Case 1: lodash vs lodash-es. This is the classic one. If you do import _ from 'lodash' and only use _.debounce, you're importing the entire library — 71.5 KB minified + gzipped. Switch to import { debounce } from 'lodash-es' and with tree shaking, you're looking at roughly 1.5 KB for just that function. That's a 98% reduction. Check it yourself on Bundlephobia.
Case 2: moment.js vs dayjs. Moment.js was the gold standard for date handling for years, but it ships at 72.1 KB minified + gzipped — and that includes all its locale data by default. Day.js has an almost identical API but comes in at 2.9 KB. That's not a typo. 72 KB vs 3 KB for basically the same functionality. The Moment.js team themselves now recommends alternatives.
Case 3: Icon libraries. This one gets people all the time. If you import { FaHome } from 'react-icons/fa', you're fine — that's tree-shakeable. But some icon libraries aren't set up for tree shaking, and you end up importing hundreds of SVG icons when you only need 5. I've seen icon imports add 200+ KB to bundles. Always check that your icon library supports tree shaking, or import icons individually.
Case 4: Date formatting libraries. Needed to format one date in a specific timezone? I once saw a project pull in moment-timezone (the full version with all timezone data) — that's 97 KB minified + gzipped — just to format a single date. The native Intl.DateTimeFormat API handles most timezone formatting natively with zero bundle cost.
The lesson here isn't "never use dependencies" — that would be silly. The lesson is: know what you're importing and how much it costs. Run npx source-map-explorer build/static/js/*.js or check Bundlephobia before adding that shiny new library. Your users on slow mobile connections will thank you.
Try It Yourself
Need to quickly clean up some messy code? Paste it into our JavaScript Formatter for instant, readable output. Ready to shrink it for production? The JavaScript Minifier strips it down to the bare minimum. And if you're not sure your code is even valid, run it through the JavaScript Validator first.