Measure the AI Cost of JavaScript UI Choices
Compare a small vanilla JavaScript counter with a framework-style counter by measuring markup, source size, imports, dependency surface, and safe AI edit context.
What you will learn
- After this lesson you can compare two UI implementations by their edit context, not only by runtime behavior.
- After this lesson you can include required markup when measuring vanilla JavaScript UI code fairly.
- After this lesson you can identify which imports and package-like APIs add context for AI-assisted edits.
- After this lesson you can build a small deterministic metric for code size, imports, dependencies, and context load.
The concrete idea: context is part of the cost
Two UI implementations can look equivalent to users but very different to an AI assistant. The assistant must understand the code it edits, the APIs it calls, and the conventions that make the change safe.
For a tiny feature, vanilla JavaScript often has a small context surface: markup, DOM selection, an event listener, local state, and a text update. A framework-style version may introduce imports, JSX-like syntax, component state, and package APIs.
That extra context is not automatically bad. It becomes worthwhile when it buys consistency, composition, testing patterns, or scale. The practical question is: for this feature, what is the minimum safe context?
Same feature: vanilla JavaScript
The vanilla version implements a counter with one button. To compare fairly, the measured sample includes both the required HTML and the script that updates it.
An AI assistant asked to change this safely mostly needs the button markup, this script, and the intended behavior. The concepts are local: find the button, update a variable, listen for clicks, and write text.
Same feature: framework-style component
The framework-style version expresses the same UI as a component. The code is still short, but safe editing may require more surrounding knowledge: how state works, what JSX-like syntax means, how the component is rendered, and which package provides the API.
In the metric below, deps means visible package/API dependency names. The framework-style sample has deps: 1 because it imports from ui-framework. Build or transpile conventions may matter in a real project, but they are not counted as package dependencies here.
A simple metric you can run
The runnable snippets do not claim to benchmark AI systems. They create a repeatable proxy: non-empty lines, import count, visible dependency count, and a rough contextScore.
The score intentionally penalizes imports and dependencies because they pull in external API knowledge. You can tune the weights for your team, but keep the same metric when comparing alternatives.
How to use this in real reviews
Before choosing a framework for a small feature, ask:
- Does the feature need shared state, routing, reusable components, or established team conventions?
- Will future changes be safer because the framework gives structure?
- Or is the feature local enough that vanilla JavaScript is easier for humans and AI tools to modify?
The best choice is not always the smallest snippet. It is the implementation with the lowest total maintenance cost for the expected future changes.
Code
<button id="count">0</button>
<script>
const button = document.querySelector("#count");
let count = 0;
button.addEventListener("click", () => {
count += 1;
button.textContent = String(count);
});
</script>This browser example includes the required HTML plus JavaScript. It is marked non-runnable because the sandbox is Node.js, not a browser DOM environment.
import { useState } from "ui-framework";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}This illustrative component shows the same behavior with framework-style state and JSX-like markup. It is non-runnable because it needs a framework package and a compatible build pipeline.
"use strict";
const samples = [
{
name: "vanilla",
deps: 0,
code: `
<button id="count">0</button>
<script>
const button = document.querySelector("#count");
let count = 0;
button.addEventListener("click", () => {
count += 1;
button.textContent = String(count);
});
</script>
`
},
{
name: "framework-style",
deps: 1,
code: `
import { useState } from "ui-framework";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
`
}
];
function nonEmptyLines(code) {
return code.trim().split("\n").filter(line => line.trim()).length;
}
function importCount(code) {
return (code.match(/^\s*import\s/gm) || []).length;
}
function tokenEstimate(code) {
return code.trim().split(/\s+/).length;
}
function contextScore(code, deps) {
return tokenEstimate(code) + nonEmptyLines(code) * 2 + importCount(code) * 20 + deps * 30;
}
console.log("name|lines|imports|deps|contextScore");
for (const sample of samples) {
console.log(`${sample.name}|${nonEmptyLines(sample.code)}|${importCount(sample.code)}|${sample.deps}|${contextScore(sample.code, sample.deps)}`);
}This standard-library Node.js program compares both implementations fairly by counting the vanilla HTML as part of the source context and counting one visible framework dependency.
import re
samples = [
{
"name": "vanilla",
"deps": 0,
"code": '''
<button id="count">0</button>
<script>
const button = document.querySelector("#count");
let count = 0;
button.addEventListener("click", () => {
count += 1;
button.textContent = String(count);
});
</script>
'''
},
{
"name": "framework-style",
"deps": 1,
"code": '''
import { useState } from "ui-framework";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
'''
}
]
def non_empty_lines(code):
return len([line for line in code.strip().split("\n") if line.strip()])
def import_count(code):
return len(re.findall(r"^\s*import\s", code, re.MULTILINE))
def token_estimate(code):
return len(code.strip().split())
def context_score(code, deps):
return token_estimate(code) + non_empty_lines(code) * 2 + import_count(code) * 20 + deps * 30
print("name|lines|imports|deps|contextScore")
for sample in samples:
code = sample["code"]
deps = sample["deps"]
print(f'{sample["name"]}|{non_empty_lines(code)}|{import_count(code)}|{deps}|{context_score(code, deps)}')name|lines|imports|deps|contextScore vanilla|9|0|0|41 framework-style|9|1|1|95
This Python program performs the same calculation, showing that the comparison idea is language-independent.
Common mistakes
✕ Comparing vanilla JavaScript code without counting the HTML it requires.
✓ Include the required markup, such as
<button id="count">0</button>, in the measured context or split the score into markup plus script for both options.✕ Treating dependency count as a vague penalty without defining it.
✓ Define exactly what is counted. In this lesson,
depsmeans visible package/API dependency names; the framework-style sample has one:ui-framework.✕ Assuming a framework implementation is always the wrong choice for AI-assisted development.
✓ Use the metric as a decision aid. Frameworks can still pay off when the app needs shared state, routing, forms, tests, team conventions, or reusable components.
✕ Sending an AI assistant only a component file when behavior depends on parent usage, framework lifecycle, or package conventions.
✓ Provide the smallest safe context: the component, relevant parent usage, dependency list, tests, and the framework patterns used by the project.
Interview questions
What does 'AI cost' mean in this lesson?
It means the amount of markup, source code, dependency knowledge, framework concepts, and project convention context an AI assistant needs to modify code safely.
Why can a short framework component still have a higher context cost?
Because the component may rely on imports, state APIs, JSX-like syntax, rendering conventions, build tooling, and project patterns that are not fully visible in the component's line count.
What is the minimum safe context for changing the vanilla counter shown here?
The relevant button markup, the script that owns the counter behavior, and the intended behavior. For the shown code, no package or build context is needed.
Why is deps set to 1 for the framework-style sample?
Because the shown code imports from one visible package/API source: ui-framework. The metric does not count a second hidden dependency unless it is explicitly defined.
When is a framework worth the extra context?
When it reduces total maintenance cost through reusable components, shared state patterns, routing, validation, tests, team conventions, or consistency across a larger app.
Practice
- Take one small UI widget from your project and list the minimum files an AI assistant would need to modify it safely.
- Extend the metric program to add a
conceptscolumn for ideas like DOM events, hooks, JSX, routing, or global state. - Rewrite the vanilla counter to support a reset button, then estimate how much additional context is needed.
Quick quiz
Key takeaways
- For AI-assisted edits, count context surface, not just source lines.
- A fair vanilla-vs-framework comparison must include required markup on both sides.
- Imports and visible dependencies add API knowledge the assistant may need.
- Tiny local UI behavior can be cheaper to explain in vanilla JavaScript.
- Frameworks can still be the better choice when structure and scale matter.
References
- Nolan Lawson — The AI cost of JavaScript frameworks (nolanlawson.com)
- MDN — Document.querySelector() (developer.mozilla.org)
- MDN — EventTarget.addEventListener() (developer.mozilla.org)
- MDN — JavaScript modules (developer.mozilla.org)