
Guide
Cursor rules for Core Web Vitals
Scope the budget to a .cursor/rules file so it loads with the files it governs, then enforce it in a pre-commit hook — rules are context, not a gate.
Published July 5, 2026
Put the numbers in a scoped .cursor/rules/*.mdc file so they load with the files they govern, then enforce them in a pre-commit hook and CI. Cursor rules are context, not a gate — they make the right choice likely, and the exit code makes the wrong one impossible. You need both layers.
What Cursor rules actually are
A rule is a markdown file with frontmatter, stored in .cursor/rules/ with an .mdc extension. The frontmatter decides when the rule enters context, and that decision is the whole design problem: a rule that is always loaded costs tokens on every request, and a rule that is never loaded may as well not exist.
There are four ways a rule gets pulled in. Always (alwaysApply: true) puts it in every request. Auto Attached loads it when a file matching globs is in play. Agent Requested lets the model pull the rule in when the description looks relevant — so that description is a retrieval key, not a comment. Manual only loads when you reference it by name.
---
description: Core Web Vitals budget for the Next.js app. Load when touching
routes, layouts, or components that render above the fold.
globs:
- "app/**/*.tsx"
- "components/**/*.tsx"
alwaysApply: false
---
# Performance budget (non-negotiable)
- Homepage First Load JS budget is **115 kB**. Run `npm run build` and read
the route table. Over budget is a regression to fix, not a number to raise.
- Server components by default. Add `"use client"` only for real
interactivity — one event handler does not justify shipping a tree.
- **Never import framer-motion from anything reachable by `app/layout.tsx`**
(Header, Footer, MobileNav). It lands on every route. Use CSS transitions,
or `components/motion/Reveal` for scroll-in.
- No new runtime dependencies without asking. A 40 kB date library to format
one string is the single most common regression in this repo.
- Images: `next/image` only, always with explicit width/height.
`priority` above the fold, `loading="lazy"` everywhere else.
- **CLS must stay 0.** Reserve space for anything that loads late: fixed
aspect boxes on media, min-heights on dynamic blocks.
- No third-party requests. Fonts self-hosted via `next/font`. No CDN
scripts, no analytics snippets, no embeds. The CSP blocks them anyway.
## When you cannot meet the budget
Say so and stop. Do not raise the budget, do not add an exception comment,
and do not disable the check. The budget is the requirement.Write rules an agent can execute, not admire
The difference between a rule that works and one that gets ignored is almost always specificity. “Optimise images” has no decision procedure attached to it. “Use next/image with explicit width and height; priority above the fold, lazy below” tells the model exactly what to type. Every rule should name an API, a threshold, or a file path.
The second thing that separates them is stating the alternative. A prohibition with no replacement leaves the model stuck, and a stuck model improvises — usually by reaching for the thing you banned and wrapping it in a comment explaining why this case is different. Every “never do X” in the rule above is followed by what to do instead.
The gate: where the rule stops being advice
Cursor rules are injected context. They shape the completion; they cannot fail it. That is not a criticism of the design — it is what context is — but it does mean the budget needs a second home outside the editor. A pre-commit hook is the cheapest place to put it.
// package.json
{
"scripts": {
"build": "next build",
"check:budget": "node scripts/check-budget.mjs"
}
}#!/usr/bin/env sh
# Cheap greps first — these catch the common regressions in milliseconds.
if git diff --cached --name-only | grep -qE '^(app/layout\.tsx|components/layout/)'; then
if git diff --cached -- app/layout.tsx components/layout/ | grep -qE '^\+.*from .framer-motion'; then
echo "✗ framer-motion imported in the layout path — it ships to every route."
echo " Use CSS transitions, or components/motion/Reveal."
exit 1
fi
fi
if git diff --cached -- 'app/**/*.tsx' 'components/**/*.tsx' | grep -qE '^\+.*<img '; then
echo "✗ raw <img> tag. Use next/image with explicit width/height (CLS)."
exit 1
fi
# The expensive one last: full build + budget check.
npm run check:budget || exit 1Order matters here. Put the regex checks first so the common failure costs milliseconds, and the full build last so you only pay for it when the cheap checks pass. A pre-commit hook that takes ninety seconds is a pre-commit hook someone will bypass with --no-verify by Thursday.
Measuring from inside the editor
Cursor supports MCP servers, configured per-project in .cursor/mcp.json or globally in ~/.cursor/mcp.json. A browser-driving server is the one worth having for this work, because it turns “is this fast?” from a question the model guesses at into one it can measure.
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}One caveat worth internalising: a local Lighthouse run under-reports LCP badly, because the simulated throttling model chains script evaluation into text paint. Use the local build for bundle size, layout shift, and third-party requests — all of which it measures honestly — and judge LCP against the deployed URL. The Claude Code version of this setup can additionally block an edit before it lands, which is the one structural difference between the two tools here.
A rules file protects a standard once you have one. Establishing the standard on a site that is currently failing is separate work, and it is what a Core Web Vitals rescue covers.
Questions
Should I use .cursorrules or .cursor/rules?
Use the .cursor/rules directory. The single .cursorrules file at the project root still works but is the legacy form, and it gives you one undifferentiated blob for every file type. Separate .mdc files let you scope a rule to the globs it actually applies to, which is what keeps the rule relevant instead of ambient.
How many rules is too many?
Watch the token cost rather than the file count. Rules marked alwaysApply are in context for every request, so an always-on ruleset of a few hundred lines is quietly taxing every single completion. Keep always-on rules to the handful of absolutes and let globs pull in the rest.
Can rules stop a bad edit from happening at all?
No — and that's the important limitation. Rules are context, so they raise the odds of compliance but cannot return a non-zero exit code. Anything that must never ship needs a pre-commit hook or a CI job, both of which run outside the editor and cannot be reasoned with.
Related notes
- Claude Code rules for Core Web Vitals
- Keeping 100 Lighthouse scores behind a strict CSP
- Cursor rules for Elementor sites
More on web development
- Claude Code rules for Elementor sites
- Claude Code rules for WCAG testing
- Cursor rules for WCAG testing
- Animating hero text without layout shift
Related service: Web Development
Want this kind of engineering on your project?
Tall Karol takes on fractional and project-based engagements for startups and agencies.
Book a working session