
What Nobody Tells You About the Permissions Policy: The Runtime Linter for Your Core Web Vitals
Stop begging for optimized assets and start using the browser’s native enforcement layer to hard-block performance-killing patterns like oversized images and synchronous XHR.
How many times have you looked at a Lighthouse report, sighed at a 4MB hero image, and realized your 20-page "Performance Guidelines" PDF is being used as a digital paperweight by the marketing team?
We spend a ridiculous amount of time setting up build-time checks. We have ESLint for code smells, Prettier for formatting, and CI/CD pipelines that scream if a bundle grows by 10kb. But the moment the site hits production, it’s the Wild West. A third-party script decides to inject a synchronous XHR request, or a content editor uploads a 5000px wide JPEG of a cat.
This is where the Permissions Policy (formerly known as Feature Policy) comes in. Most developers think it's just for disabling the camera or microphone, but it actually contains a hidden suite of "performance policies" that act as a runtime linter for your Core Web Vitals.
Instead of asking people to be fast, you can literally program the browser to refuse to be slow.
The "Enforcement over Education" Philosophy
I used to be a big believer in education. "Just teach the team about WebP!" I’d say. I was wrong. Education is great, but automation is better.
The Permissions Policy header allows you to define a set of rules that the browser must follow while rendering your site. If a script or an asset violates these rules, the browser either blocks the behavior or throws a loud error in the console. It’s the ultimate "safety net" for Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS).
1. Killing the 5MB "Thumbnail"
The oversized-images policy is my personal favorite. It’s the solution to the "User uploaded a print-res photo for a 100px avatar" problem.
When enabled, the browser checks if the intrinsic dimensions of an image are significantly larger than its display container. If the image is, say, 3x larger than it needs to be, the browser can downscale it or simply refuse to render the bloated version during development.
Permissions-Policy: oversized-images=()By setting this to () (an empty list), you are essentially saying "No oversized images allowed." In a world where LCP is king, preventing a multi-megabyte image from hijacking the main thread is a massive win.
2. Mandatory Image Dimensions (Bye-Bye CLS)
Cumulative Layout Shift is usually caused by images loading without defined dimensions, forcing the browser to recalculate the layout once the file header is parsed.
We’ve all told our teams: "Always add width and height attributes." Does it happen? Rarely.
The unsized-images policy enforces this. If you try to load an image without dimensions, the browser will give it a default size (usually 300x150) or force it to behave, making the layout shift obvious during the dev phase rather than a surprise in your CrUX report.
Permissions-Policy: unsized-images=()3. The Death of Synchronous XHR
There is no reason—zero, zilch, nada—to use synchronous XMLHttpRequest in 2024. It locks the main thread, freezes the UI, and makes your site feel like it’s running on a toaster. Yet, some legacy third-party tracking scripts still use it.
You can kill it site-wide with one line:
Permissions-Policy: sync-xhr=()If a script tries to call xhr.open(method, url, false), the browser will throw a DOMException. It’s aggressive, yes, but your Interaction to Next Paint (INP) scores will thank you.
How to actually deploy this (without breaking everything)
You don't want to just flip these on in production and watch your site turn into a graveyard of broken image icons. The best way to use these is via the `Permissions-Policy-Report-Only` header.
This works exactly like Content Security Policy (CSP) reporting. The browser won't block the asset, but it will send a JSON payload to your reporting endpoint whenever a violation occurs.
The "Safe" Implementation Strategy:
1. Monitor: Use the Report-Only header to see how many "crimes" are currently being committed on your site.
2. Fix: Use that data to optimize images or update legacy scripts.
3. Enforce: Once the reports run dry, switch to the standard Permissions-Policy header to prevent regressions.
Here is what a comprehensive, performance-focused header looks like for an Express.js app:
app.use((req, res, next) => {
res.setHeader(
'Permissions-Policy',
'oversized-images=(self), ' +
'unoptimized-images=(self), ' +
'unsized-images=(self), ' +
'sync-xhr=(), ' +
'picture-in-picture=*, ' +
'geolocation=()'
);
next();
});The "Gotchas" You Need to Know
I’d love to tell you this is a silver bullet, but there are a few things that might trip you up:
* Browser Support: This is heavily driven by Chromium. Chrome, Edge, and Brave support these performance policies well. Safari and Firefox are... let's just say they have "differing priorities" regarding these specific performance flags. However, since most Core Web Vitals data comes from Chrome users anyway, it's still worth implementing.
* The 2x Threshold: For policies like oversized-images, Chrome typically doesn't complain unless the image is at least 2x or 3x the container size. It gives you some breathing room for high-DPI (Retina) displays.
* Third-Party Iframes: If you use a lot of third-party iframes (like YouTube or legacy widgets), you might need to explicitly allow certain features within those iframes using the allow attribute:
<iframe src="https://example.com" allow="sync-xhr 'none'; oversized-images 'none'"></iframe>Final Thoughts
The Permissions Policy is effectively a "Performance Contract" you sign with the browser. It moves performance from a "best effort" conversation to a "hard requirement."
Stop playing whack-a-mole with unoptimized assets. Start using the platform to defend your main thread. It feels a bit mean the first time you block a script or a giant image, but your users (and your SEO rankings) will be much happier for it.

