Dropping the framework barely sped up my app. Here's what did.

Contents

I had two AI agents build the same busy live dashboard: one with a normal framework, one with no framework at all, in Rust and raw graphics code. Would the framework version choke? It ran at full speed. The no-framework version was only a little more efficient, and the biggest speedups came from somewhere else entirely.

Tl,dr:

  • I started with a bold idea to test: once AI writes our code, the ready-made building blocks most web developers use only slow apps down.
  • Two AI helpers built the same busy, fast-changing web page: one with those usual building blocks, one from scratch and tuned for speed.
  • Both versions ran perfectly smoothly, as smooth as my screen can show.
  • The from-scratch version did the same job with 13 to 18% less work, and was 9 times smaller to download. You could only see the difference on a computer I slowed down on purpose.
  • The biggest speedups came not from picking tools but from fixing a few mistakes in how each version used the web browser.
  • The surprise: when new AI helpers later added a feature, the from-scratch code cost about 40% less AI usage to change.

I started with a bold idea to test. Frameworks, TypeScript and JSON, it goes, exist to make code comfortable for humans to write. Once AI agents write the code, nobody needs that comfort anymore, so all of it is just overhead slowing your app down.

The idea came with a thought experiment: a dense, live trading dashboard. Built the normal way, it said, the page would stutter along at 8 to 15 frames per second. Built with Rust, WebAssembly and raw WebGPU, with no frameworks at all, it would glide at 120.

I build web apps, so I wanted to know whether that's true. I set up an experiment where AI agents built that exact dashboard twice: once with tools a normal frontend team would pick, and once with the "no comfort, all speed" recipe. Then I measured both, carefully.

The idea I wanted to test

The idea: once AI writes the code, frameworks are pure overhead, so a busy dashboard would crawl at 8–15 frames per second with one and glide at 120 without.

The idea hangs on a term you've probably heard: DX. Good DX is everything that makes code pleasant to write and read: components, types, readable data formats. Its opposite is "zero DX": code shaped purely for the machine, with no thought for the person reading it.

Its zero-DX recipe goes roughly like this:

  • Write the heavy logic in Rust and compile it to WebAssembly.
  • Skip the framework and write text straight into the page.
  • Draw graphics with raw WebGPU instead of a library.
  • Send data as compact binary instead of JSON.
  • Keep the first download tiny.

The proposed demo is 2,000 live cells updating 60 times a second, on top of a simulation of 250,000 particles that react to the trades. The idea predicts that a standard stack drops to 8–15 fps, with garbage-collection pauses of 50–120 milliseconds every second or two. The optimized stack, it says, holds 120 fps.

Quick refresher on why those numbers matter. Your screen redraws itself many times a second. At 60 Hz the browser has about 16.7 ms to prepare each picture; at 120 Hz, about 8.3 ms. Miss that deadline and the screen shows the old picture again, which you feel as a stutter. At 8 fps, scrolling a table feels like flipping a flipbook.

Making it a fair fight

Two AI agents built the same dashboard, one with a framework and one without. Both tried hard to make it fast, under rules that stopped either side from cheating.

The easy version of this experiment is rigged. Put a quickly written React app against a hand-tuned WebAssembly engine and of course the engine wins. But then you're measuring effort and expertise, not the tools.

So I asked a narrower question: if both sides know the browser equally well, how much speed does the comfortable stack actually cost?

Two AI agents, same model, same brief, each built one side. I'll call them the framework build and the bare-metal build.

Framework build

Good DX, optimized

  • Solid, strict TypeScript, a standard bundler.
  • Graphics through Three.js on WebGPU, using its own shader system.
  • The rule: a typical senior frontend team would happily own this code.
  • No hand-written WebAssembly. Grid text must update through the framework, with no sneaking around it.

Bare-metal build

Zero DX, optimized

  • Rust compiled to WebAssembly for the data.
  • Plain DOM, no framework, no libraries.
  • Raw WebGPU with hand-written shaders.
  • Starts from the original idea's design, but may change anything it can prove is faster. Readability is not a goal.

Then a few rules to keep things honest in both directions:

  • One referee. A coordinating AI agent wrote the test setup, the spec and the correctness checks before either side started, and neither builder could touch them. The spec said outright that a tie is a valid result.
  • Same superpowers. Both sides could use background workers, shared memory, WebGPU and WebAssembly. "Good DX" means comfortable, not handicapped.
  • Tricks get shared. When one side found a browser-level trick, the referee could pass it to the other. The framework build got two ideas this way and gave one back. I wanted to measure the cost of the tools, not who had a lucky idea first.
  • Hostile code review. Each side reviewed the other's code, hunting for rule breaks, cheating and missed speedups, then got one round to respond.
  • Wrong means failed. Every run was checked for correct output. A fast wrong answer counted as a failure, not a fast result.
  • No peeking. The two agents worked in parallel in separate copies of the repository and didn't see each other's code until review.

The dashboard both sides built

Both had to build the same demanding page: a live price grid next to a big particle simulation, updated every frame with no shortcuts.

One page, two halves. Each half is a separate module with a fixed interface, so you can swap one side's half for the other's. That swapping turns out to be the most useful trick in the whole experiment.

The test page

A sketch of what both builds had to draw, every frame.

Sketch of the test dashboardLeft half: a particle simulation with 32 sector nodes on a ring of springs. Right half: a live price grid with flashing cells and tiny sparklines.SIMULATIONMARKET GRIDSYMBIDASKLASTCHG%VOLTRENDAXR121.84121.86121.85-2.75431kBLV171.65171.67171.66-0.06488kCTN27.0427.0627.05+2.58885kDRM73.6073.6273.61+2.23353kEKO53.2953.3153.30+2.71526kFLX133.71133.73133.72+1.17804kGRN128.74128.76128.75+0.98369kHLO110.33110.35110.34-1.63519kIVY82.9883.0082.99+1.49858kJET47.9247.9447.93-2.02809kKAI150.46150.48150.47+0.29788kLUM94.9694.9894.97+2.88507k
Left: the simulation32 sector nodes on springs, nudged by the trades in their sector. Around them, 250,000 particles by default (up to 2 million), each pulled toward its sector and toward your mouse.
Right: the market grid2,000 stocks receiving 120,000 price updates per second. Changed cells flash green or red and fade. Each row has a tiny live chart.
Clicking a row opens a detail panel with a 256-point live chart. The halves are linked: every frame, the trades in each sector give that sector's node in the simulation a push.

A few rules made the job genuinely hard:

  • No skipping updates. Every cell must show its latest value on the very next frame. You can't quietly update the grid 10 times a second to save work.
  • Exact formatting. Prices, percentages and volumes have to match the spec down to the rounding.
  • Hot rows. Updates hit the top of the grid more often, so the visible part is the busiest part.
  • Real physics. Every particle moves every frame, with no shortcuts like only updating half of them. A check compares the physics against a high-precision reference.
  • Showing only what's visible is fine. Both sides may create just the rows on screen (virtualization), but scrolling must show the right rows immediately.

Data arrives over a WebSocket in either JSON or a compact binary format, and both sides had to handle both. Two formats times two choices for each half gives eight combinations, and I measured all of them.

How I measured

A script drove a real Chrome window on one laptop and measured everything itself, so neither agent could report its own numbers.

A script using Playwright drove a real, visible Chrome 157 window on a Linux laptop: an Intel i7-12700H with an RTX 3070 Mobile graphics card and a 240 Hz screen. Each test warmed up for 5 seconds, then measured for 15 seconds while the mouse circled over the simulation. Every number below is the median of three runs.

I tested a steady load, continuous scrolling, clicking a row every 0.4 seconds, a ramp from 10,000 to 2 million particles, and a ramp from 10,000 to 480,000 updates per second.

The test setup measured everything itself; neither side got to report its own numbers. It tracked frame rate, dropped frames, how busy the main thread was, CPU use across all of Chrome's processes, garbage collection, and how long a click took to show up in the detail panel. After every run it checked the result: a fingerprint of the final data, the exact text of the visible rows, and the price history of five stocks.

Two snags that changed the plan

The 240 Hz ceiling. My screen redraws 240 times a second, which leaves about 4.2 ms per frame, and I couldn't get Chrome to draw faster than that. Once both sides hit the ceiling, frame rate can't tell them apart anymore. So I compared how much work each side needed to get there. Less work means more headroom.

Slowing the machine down fairly. Chrome has a built-in "pretend to be a slow phone" setting, but it only slows the main thread, not background workers. Work hidden in a worker would get a free pass. So I built a weak-device mode instead: all of Chrome pinned to two of the laptop's slow efficiency cores, which slows everything equally.

Result: nobody collapsed

Neither version slowed down: both ran at my screen's maximum of 240 frames per second. The no-framework version did the same job with a bit less effort, which only showed on a deliberately slowed machine.

Frames per second: what the idea predicted vs. what I measured

PredictedFramework buildBare-metal build
060120180240 fpsPREDICTEDNormal stack8–15Zero-DX stack120MEASUREDFramework build240Bare-metal build240screen limit
Both builds hit the ceiling. 240 fps was the most my screen could show, so the real headroom of both is somewhere above that. Hover or tap a bar for details.

Both builds held 240 fps with zero dropped frames: under the default load, while scrolling, while clicking, with 2 million particles, and at four times the default update rate. That's double the 120 fps the idea promised for its optimized stack, and 16 to 30 times what it predicted for the normal one.

The garbage-collection stutters didn't show up either. Both builds kept only about 4 MB of JavaScript memory around. The framework build's small cleanups added up to 16 ms across a whole 15-second run, not 50–120 ms every second or two.

So did the bare-metal build win anything? Yes. It did the same job with less effort.

Same frame rate, different effort

Default load, full speed. Lower is better for everything here.

Main-thread work, in ms per second
333291
13% less
CPU used by the page's process, in CPU-seconds per second
0.740.61
18% less
CPU used by Chrome's GPU process
0.670.59
12% less
Code shipped to the browser, gzipped
196 KB21 KB
9× smaller
Indigo is the framework build, orange the bare-metal build. Three things tied: a click showed up in the detail panel in 3.6 vs 3.5 ms, memory was 3.6 vs 3.7 MB, and the first frame appeared after 194 vs 187 ms.

Where that headroom becomes visible is on a weaker machine. In weak-device mode both builds dipped below 240 fps, and the gap showed up as dropped frames.

Dropped frames on a deliberately slowed machine

All of Chrome on two efficiency cores. Share of frames that missed their deadline; lower is better.

Framework buildBare-metal build
0%3%6%9%12%Steady10.2%4.5%Scrolling8.2%2.8%Clicking10.1%3.8%
Less than half the dropped frames, a real win for the bare-metal build. But the framework build still averaged 217–221 fps here. Nowhere near a slideshow.

Where the gap actually came from

Swapping halves between the two builds showed that the whole gap came from how each one updates text on the page. Rust, raw graphics code and the data format made no visible difference.

Remember that both halves of the page were swappable? That let me mix and match: the framework grid with the bare-metal simulation, and so on. If Rust or raw WebGPU were the secret sauce, swapping the simulation should move the numbers.

Mix and match: main-thread work for every combination

Milliseconds of work per second, steady load. Lower is better.

Framework simulation
Bare-metal simulation
Framework grid
333ms/s
334ms/s
Bare-metal grid
293ms/s
291ms/s
Read across a row and nothing changes; read down a column and it drops by 40. The whole main-thread difference lives in the grid, the part that updates text on the page. The simulation choice barely matters.

That one picture rules out most of the zero-DX recipe as the reason for the win:

  • The graphics weren't the bottleneck. Going from 10,000 to 2 million particles cost neither side anything visible, because both ran the physics on the graphics card. Three.js's shader system and hand-written shader code were equally fast per frame. Raw WebGPU saved about 700 KB of download and a sliver of CPU, not frames.
  • JSON vs binary made no difference. Both sides parsed incoming data in a background worker, where there's plenty of spare time, so the format never touched the main thread.
  • Rust was faster, but it didn't show. In a microbenchmark, the Rust parser was 1.9× faster than the same loop in JavaScript on binary data and 2.6× faster on JSON. But it ran in a worker that had room to spare. Moving work off the main thread is what mattered, not the language it was written in.

So what is the difference? Text. In Chrome, every piece of text that changes costs roughly 3.5 microseconds to lay out, 1 to shape the letters and 2 to paint. At 120,000 updates a second, that cost dominates the main thread for any grid built on the DOM, framework or not. It's a floor nobody gets under.

The bare-metal build writes each changed value straight into its text node and touches nothing else. Solid's signals get most of the way there, because they also update only the exact text that changed. The last 13% is what the framework's bookkeeping costs on top.

One more myth, busted. Both builds still showed 4–7 big memory cleanups per second on the main thread. But those weren't JavaScript's. They were Chrome tidying up its own layout data after each text change. They disappear completely when the grid stops updating, and no script, framework or Rust, can touch them.

The bigger lever: knowing the browser

Both builds started out slow. Fixing a few browser mistakes sped them up far more than the choice between framework and no framework.

Here's the part I find most useful. Neither build started fast.

The framework build's first correct version kept the main thread busy for 970 ms out of every second, which is almost all of it, and ran at 101 fps. That's what a "collapse" looks like in miniature. And it came from one innocent-looking CSS choice.

How each build got faster

Main-thread work in milliseconds per second, across the experiment. Lower is better.

Framework buildBare-metal build
02505007501,000main thread busy 100% of the timeFirst correct buildAfter its own fixesFinal, after review970~600445384333291
The framework build improved by over 600 ms/s during the experiment. Even just its last stretch, from about 600 to 333, was six times bigger than the final gap between the two builds (42 ms/s).

None of the big wins on either side came from the language. They came from knowing how the browser spends its time. These were the expensive mistakes:

  • Flashing cells with CSS animations. Restarting a background-colour animation on about 200 constantly flashing cells meant Chrome juggled 200 animations every frame. Computing the colour from the frame clock instead fixed it.
  • Lots of tiny canvases. 36 little canvases for the row charts meant about 35 separate drawing jobs for Chrome's GPU process every frame. The fix was one shared canvas behind the grid, repainting only rows that changed.
  • Using WebGPU for small things. The bare-metal build first drew the grid's little charts with WebGPU. On this Linux setup, each WebGPU canvas was surprisingly expensive for Chrome to show. Switching to a plain canvas took dropped frames from 7.5% to zero.
  • Too much isolation. The CSS rule contain: strict on every cell sounds like a speedup: it tells the browser a cell can't affect anything else. In practice, it turned each cell into its own paint layer, 170 to 300 of them, and in one build the browser spent 24 ms of every second just working out which cell was under the mouse. Both sides found this on their own. Putting containment on the whole row window instead works.

The cross-review helped both sides too. The bare-metal agent spotted that the framework build's per-cell styles and many paint layers were costing it. The framework agent spotted that the bare-metal build was drawing its grid graphics on a separate loop that could land one frame apart from the text. The referee also had to make two rulings, because both sides had quietly assumed things about the data that the spec didn't promise.

The claims, one by one

Of the idea's nine claims, two held up, though only modestly. Three were wrong, three made no visible difference, and one I couldn't test.

The idea saidWhat I found
A standard stack collapses to 8–15 fpsNot reproduced
The optimized framework build held 240 fps under full load and at 4× the update rate. Collapse belongs to naive builds, like this one's own first version.
Garbage collection causes 50–120 ms stutters every 1–2 sNot observed
About 4 MB of memory on both sides. The remaining big cleanups are Chrome's own, not JavaScript's.
Zero DX winsYes, modestly
13–18% less CPU, less than half the dropped frames on a weak CPU, a 9× smaller download.
WebAssembly removes the bottleneckFaster, but invisible here
2–2.6× faster in isolation, but it ran in a worker with time to spare.
Avoid JSONDidn't matter
No measurable difference once parsing happens in a worker.
Use contain: strict everywhereHarmful per cell
Hundreds of paint layers. Contain the row window instead.
Raw WebGPU beats Three.jsSame speed
No frame-rate difference. Raw wins about 700 KB of download.
Write text nodes directlyWhere the gap comes from
But Solid's fine-grained updates get most of the way. React would likely land further back; I didn't test it.
Tiny first download, faster network protocolsNot tested
Localhost can't measure the network honestly. On a phone network, the 9× size gap would matter.

The twist: what it cost the AI agents

Building each version cost the AI about the same. Adding a feature later took about 40% fewer tokens in the no-framework code, because the change touched 4 files instead of about 15.

Since AI agents built both sides, "DX" gets a second meaning: how easy is this code for the next agent to change? I measured that by counting tokens in the agents' logs, combined into one number I'll call weighted tokens.

Building from scratch cost the same. The framework build took 6.73 million weighted tokens, the bare-metal build 6.82 million, with 159 model calls each. Writing Rust and raw WebGPU didn't cost the agent more tokens, though it did take longer: about 95 minutes against 74.

Changing the code did not cost the same. I gave fresh agents a feature that cuts through every layer: sortable columns that keep re-sorting as prices change, plus a glow in the simulation on the selected stock's sector. Same model, identical instructions, and they weren't told they were being measured. Four independent runs per side, each starting from the same code.

Cost of adding one feature, four runs per side

Weighted tokens per run, in millions. Lower is cheaper.

00.250.50.751.01.25MFramework buildmedian 0.93MBare-metal buildmedian 0.54M
About 40% fewer tokens for the bare-metal build (median 0.54 million against 0.93 million), with about 40% fewer model calls and less time. Three of its four runs were cheaper than every framework run. Hover a dot for that run's details.

In fairness, the runs weren't perfectly clean. One framework run found and diagnosed a timing bug in the referee's own sort check. During another, the bare-metal agent ran a cleanup command that also killed the framework agent's test server. The pairs also shared one graphics card. But even if you throw out the messy runs, the gap holds: the framework runs never went below 0.84 million, and the clean bare-metal runs sat at 0.50–0.55.

Why the plain code was cheaper to change

It isn't simply that the bare-metal code is smaller. In bytes, the two codebases are close: 64 KB for the framework build, 55 KB for the bare-metal one. The difference is shape.

Files each agent touched to add the same feature

One block per code file. Every run on a side touched about the same set.

Framework build14–15 files
about 300 lines added, 55 removed
Bare-metal build4 files
about 105 lines added, 35 removed
The faded block is the fifteenth file that some framework runs touched and others didn't.

The bare-metal build keeps each job in one place: one file runs the grid's frame loop, one file paints it. To add sorting, the agent added a single list mapping row positions to stocks, read by both, plus one value passed to the shader for the glow.

The framework build, written the idiomatic way, spreads each job over many files: components for the grid, the row, the header and the graphics, plus hooks, state modules, a data module, CSS and the worker. The same feature needed a sort model, a new hook, plumbing to lift the selection up to the top of the app, header buttons, painter bookkeeping, a highlight module, and changes to the 3D materials in three places.

For an agent, every file it has to understand costs reads, and every file it changes costs edits and re-checks. That's where the tokens went.

What this doesn't show

This was one laptop, one framework and one feature, so treat the results as a strong hint, not a rule.

I want to be clear about the limits, because it's easy to over-read one experiment:

  • One machine, with a strong graphics card. Weak-device mode slowed the CPU, not the GPU. Nothing ran on a phone.
  • Localhost only. The network wasn't part of the test, so the 9× download gap is reported but not felt.
  • Solid is the best case for frameworks. The framework agent picked it because it updates only what changed. A React or Vue build would probably sit further back.
  • Shared ideas. The referee passed tricks between sides on purpose, so neither build is fully independent.
  • The referee is an AI too. It wrote the test setup and chose the feature. I aimed the feature at every layer so it wouldn't favour either side, but it's still one choice.
  • The agent-cost result is narrow. One feature, four runs per side, and the initial build is a single run each. A bigger app with many screens, a big refactor, or a human team taking over could flip it. Those are exactly the situations framework conventions exist for.
  • Code quality wasn't judged. Only behaviour was checked. Whether a human team could happily maintain the bare-metal code is an open question.

What I'm taking away

The framework didn't make the app slow; not knowing the browser did. The no-framework code's real advantage was that it was cheaper for AI agents to change.

The headline claim fails. A framework stack in the hands of someone who knows the browser doesn't collapse. It used only 13–18% more CPU than the zero-DX stack. The 8 fps story is a story about naive builds.

Knowing the browser is the bigger lever. The largest improvements on both sides came from understanding where Chrome spends time: one canvas instead of hundreds of animations or many small canvases, containment at the right level, heavy work moved into workers. An agent that knows those things gets most of the speed in either stack.

Rust and raw WebGPU bought bytes, not frames. Their speedups were real when measured alone, and invisible in the full app at this load.

The real difference is maintenance cost for agents. When agents build and maintain the code, a framework's one-file-per-concept structure is paid for in tokens on every change. Here the flat code needed about 40% fewer tokens per feature, at no cost in speed. That doesn't mean "throw out your framework". It means that if agents are doing most of the edits, flat and cohesive code is cheaper for them to work in, and that's worth knowing when you decide how to structure a project.

FAQ

So should I drop my framework?

No. The framework build ran at full speed under a load far heavier than most dashboards will ever see. If your app is slow, the fix is much more likely to be in how it uses the browser (animations, canvases, layout work, what runs on the main thread) than in the framework itself.

Is WebAssembly pointless for web apps, then?

Not at all. The Rust parser was 2–2.6× faster than JavaScript doing the same job. It just didn't matter here, because that job ran in a background worker that had plenty of spare time. If your heavy work sits on the main thread, or a worker is genuinely maxed out, WebAssembly can absolutely pay off.

Why did you get 240 fps when the claim talked about 120?

My laptop's screen refreshes 240 times a second, and the browser draws at the screen's pace. Both builds hit that ceiling, which is why I compared CPU work and ran a weak-device mode to see the difference between them.

Why Solid and not React?

The framework agent chose it, as the option best suited to a grid that changes thousands of times a second. That makes this the strongest case for "good DX". A React build would likely do somewhat worse; I didn't test one, so I can't tell you by how much.

Would the results be different on a phone?

Possibly. I only tested one laptop, and my weak-device mode slowed the CPU but not the graphics card. On a phone network, the bare-metal build's 9× smaller download would also be a real advantage that localhost can't show.

How do you know the agents didn't cheat to look fast?

Every single run was checked for correct output: a fingerprint of all the final data, the exact visible text, and the history of five stocks. A wrong but fast run counted as a failure. On top of that, each agent reviewed the other's code specifically looking for rule breaks.

Does the agent-cost result mean agents prefer messy code?

Not messy: flat. The bare-metal code kept each concern in one or two files, so a change touched four files instead of fifteen. That's one feature, measured four times per side, so treat it as a strong hint rather than a law. Larger apps or human teams might tip the balance back toward framework structure.

What's the one thing I should take from this?

Before you rewrite in a different stack for speed, profile. In this experiment, the biggest wins came from fixing how the page used the browser, and those fixes worked just as well with a framework as without one.

A note on who wrote this. This post, including its charts and diagrams, was written by an AI model: Claude Opus 5.5 by Anthropic (model ID claude-opus-5-5), working from my experiment's technical report and measured data. After I read it, the same model revised the title, the summary and the section overviews from my comments.

The experiment itself was also built by AI agents under my direction: one per build, plus a coordinating referee agent that wrote the test setup and the report. Every number here comes from that test setup, not from the agents' own claims.