How we complicated web development so much that browsers had to add debugging tools for our debugging tools
The Absurd Reality We’ve Accepted
Open your browser’s DevTools right now on any modern web application. Click on a JavaScript file. See that little {...}
button that says “Pretty print”? Notice the “Original” vs “Deployed” toggle? Check the Network tab – see those .map
files that are often larger than the actual code?
We’ve normalized insanity.
We write code that browsers can’t understand, transform it into code humans can’t understand, then ship maps so we can pretend we’re debugging the code we actually wrote. This is not progress. This is a cry for help.
How Did We Get Here?
Act I: The Simple Times
<!-- 2005: How we built web apps -->
<script>
function updateCounter() {
var count = parseInt(document.getElementById('counter').innerText);
document.getElementById('counter').innerText = count + 1;
}
</script>
<button onclick="updateCounter()">Click me</button>
<span id="counter">0</span>
Debugging process: Open DevTools, set breakpoint, done.
Act II: The “Improvement”
// 2024: How we build web apps now
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { incrementCounter } from '@/store/slices/counterSlice';
export const Counter: React.FC<CounterProps> = ({ initialValue }) => {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<Button onClick={() => dispatch(incrementCounter())}>
<CounterDisplay value={count} />
</Button>
);
};
Debugging process:
- Find the minified code (
bundle.fd4a5.js:1:48573
) - Click “Original” to load source map (300KB download)
- Navigate through 15 layers of framework code
- Set breakpoint on line that may or may not correspond to actual execution
- Watch variables named
_a
,_b
,_c
instead of your names - Give up and add
console.log
everywhere
Act III: The Admission of Defeat (Source Maps)
// What actually runs in the browser:
var _s=_$RefreshSig$(),_s2=_$RefreshSig$();import{jsx as _jsx}from"react/jsx-runtime";
const _=_s((p,d)=>{_s();const[c,s]=(0,__REACT_IMPORTED__.useState)(0);return/*#__PURE__*/_jsx("div",
{onClick:()=>s(c+1),children:c});},"useState{[c,s](0)}");_c=_;var _c;$RefreshReg$(_c,"Counter");
// 2MB source map to translate this back to your 10 lines of readable code
Enter Juris: The Emperor Has No Clothes
Here’s the same counter in Juris:
// This IS the code that runs. No transformation. No source maps.
const juris = new Juris({
states: { count: 0 },
layout: [{span: {
text: () => juris.getState('count', 0)
}},
{button: {
text: 'Click me',
onclick: () => juris.setState('count', juris.getState('count', 0) + 1)
}}
]});
juris.render();
Debugging process: Open DevTools, see your actual code, set breakpoint, done.
The Monument to Our Failure
Source maps are literally:
- Admission that our code is unreadable
- Proof that we’ve over-complicated everything
- Band-aid on a self-inflicted wound
- 3x larger than the original code
- Another layer of complexity to manage
The Insane Statistics
Typical React Application:
bundle.js: 250KB (minified, unreadable)
bundle.js.map: 800KB (to make it "readable")
vendor.js: 450KB (framework code)
vendor.js.map: 1.2MB (more maps)
polyfills.js: 30KB (for features browsers have)
Total: 2.7MB of which 2MB is just to debug the mess we created
Same App in Juris:
juris.js: 45KB (readable)
app.js: 5KB (your readable code)
Total: 50KB of actual, debuggable code
The Build Tool Industrial Complex
We’ve created an entire industry around our bad decisions:
{
"devDependencies": {
"webpack": "^5.89.0", // To bundle
"babel": "^7.23.0", // To transpile
"typescript": "^5.3.0", // To add types we'll strip
"ts-loader": "^9.5.0", // To load the types
"source-map-loader": "^4.0.0", // To load the maps
"terser-webpack-plugin": "^5.3.0", // To minify
"css-loader": "^6.8.0", // To import CSS in JS
"style-loader": "^3.3.0", // To inject the CSS
"sass-loader": "^13.3.0", // To compile SCSS
"postcss-loader": "^7.3.0", // To transform CSS
// ... 47 more tools to fix problems created by other tools
}
}
Every single one of these exists because we refuse to write code browsers understand.
The Debugging Experience We Lost
Debugging in 2005:
Error: undefined is not a function
at updateCounter (app.js:12) <-- Your actual code
at HTMLButtonElement.onclick (index.html:45) <-- Where you attached it
Debugging in 2024 (With Frameworks):
Error: Cannot read properties of undefined
at _0x4a3f (vendor.2f4a5d.js:1:458293)
at Array.reduce (<anonymous>)
at _redux_internal_dispatch (vendor.2f4a5d.js:1:234567)
at _wrapped_dispatch_action_payload (vendor.2f4a5d.js:1:345678)
at Object._0x5b2c (app.f5d4a.js:1:23456)
at _react_fiber_reconcile_children (vendor.2f4a5d.js:1:567890)
... 23 more internal framework calls
(Click here to maybe see original location if source map loads)
Debugging with Juris:
Error: undefined is not a function
at updateCounter (app.js:12) <-- Your actual code, because it IS the actual code
The Problems Source Maps “Solve”
“I can’t read my minified code!”
- Maybe don’t minify it? Juris: 45KB readable > 25KB minified + 800KB source map
“I need to debug my TypeScript!”
- Browsers run JavaScript. Write JavaScript. Problem solved.
“JSX is easier to read!”
- Is it? Or have we just Stockholm Syndrome’d ourselves?
<em>// JSX</em>
<div onClick={handleClick}>{count}</div>
<em>// Juris</em>
{div: {onclick: handleClick, text: count}}
<em>// One compiles to garbage. One just runs.</em>
“But developer experience!”
- Waiting 30 seconds for builds is good DX?
- Debugging through 3 abstraction layers is good DX?
- 1000 npm packages is good DX?
What Browsers Can Already Do (That We Ignore)
Modern browsers support:
- ES Modules (no bundling needed)
- Async/await (no regenerator-runtime)
- Optional chaining (no Babel)
- CSS nesting (no Sass)
- Web Components (no framework)
- Proxy objects (reactivity)
- URLSearchParams (routing)
We have the technology. We just refuse to use it.
The Juris Philosophy: Radical Simplicity
// Entire reactive app, no build step
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/juris@latest/juris.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const juris = new Juris({
states: {
todos: [],
filter: 'all'
},
components: {
TodoApp: (props, ctx) => ({
div: {
children: [
{h1: {text: 'Todo App'}},
{input: {
placeholder: 'Add todo',
onkeydown: (e) => {
if(e.key === 'Enter') {
ctx.setState('todos', [...ctx.getState('todos'), {
text: e.target.value,
done: false
}]);
e.target.value = '';
}
}
}},
() => ({ul: {
children: ctx.getState('todos').map(todo =>
({li: {
text: todo.text,
style: {textDecoration: todo.done ? 'line-through' : 'none'},
onclick: () => {
const todos = ctx.getState('todos');
todo.done = !todo.done;
ctx.setState('todos', [...todos]);
}
}})
)
}})
]
}
})
}
});
juris.render();
</script>
</body>
</html>
That’s it. That’s the entire app. No build. No maps. No tools.
The Real Cost of Complexity
What We’ve Lost:
- Instant feedback (save โ refresh โ done)
- True understanding of what runs in the browser
- Simplicity of View Source โ Learn
- Performance (50KB vs 2MB)
- Debuggability (actual stack traces)
- Accessibility for new developers
What We’ve Gained:
- Node modules folder with 50,000 files
- Build times measured in minutes
- “JavaScript fatigue”
- Security vulnerabilities in our build tools
- The need for source maps
The Path Forward
Option 1: Keep Adding Complexity
- Wait for faster build tools (esbuild, swc, turbopack)
- Add more debugging tools for our debugging tools
- Create AI to understand our unreadable code
- Accept 5MB of JavaScript for a todo app
Option 2: Return to Sanity
- Write code browsers understand
- Ship code humans can read
- Debug code that actually runs
- Use frameworks that enhance, not replace, the platform
Conclusion: The Monument Must Fall
Source maps aren’t a feature. They’re a confession. They’re proof that we’ve lost our way so badly that browsers had to add a feature just to help us understand our own code.
Every .map
file is a tombstone for simplicity. Every “Original” button in DevTools is an admission of defeat. Every second spent waiting for builds is time stolen from creating.
Juris doesn’t need source maps because it doesn’t create problems that need solving.
When you write code the browser understands:
- Your code IS the source
- Your bugs have real stack traces
- Your debugging is actual debugging
- Your 45KB framework does what their 500KB can’t: just work
The monument to our collective failure stands tall in every node_modules
folder, in every webpack config, in every source map. It’s time to tear it down.
Write code for the platform. Ship code for humans. Debug code that exists.
The browser is more capable than ever. Maybe it’s time we were too.
Try Juris: https://jurisjs.com – No build required. Ever.
View source on this page. Understand it. That used to be normal.
Leave a Reply