I work at a digital agency and lead the tech side. One thing we try to get right from day one is performance. Not because we're obsessed with Lighthouse scores. Because we've learned that performance problems don't come from bad code. They come from bad decisions made early in the project.
The CMS you choose. How you handle third-party scripts. Whether you serve videos as raw iframes or defer them. How your CSS is loaded. Where your assets live.
These are architecture decisions. And once they're made, they're expensive to undo.
The Pattern We Try to Avoid
Here's what happens on a lot of projects across the industry. A site launches. The design looks great. Then someone runs a Lighthouse audit and the score is 43. Panic. Everyone wants to "fix performance" like it's a bug you can patch on a Friday afternoon.
It's not a bug. It's the consequence of choices nobody questioned months ago.
We try not to end up there. That means asking the uncomfortable questions at the kickoff, not at the QA stage. It means pushing back sometimes. And it means being honest about what certain tools and platforms cost you in performance before you commit to them.
Your CMS Choice Is a Performance Decision
This is the one that doesn't get enough attention. People choose a CMS based on features, familiarity, or client preference. Performance rarely enters the conversation. But it should. Because some platforms give you a head start and others put you in a hole from the beginning.
A headless CMS like Sanity, Contentful, or Storyblok gives you full control over the frontend. You decide what gets loaded, when, and how. There's no theme layer injecting scripts you didn't ask for. No plugin ecosystem silently adding database queries and stylesheets on every page load.
WordPress can absolutely be fast. I've built fast WordPress sites. But it takes discipline. The default experience is a theme that loads everything everywhere, a page builder that inlines CSS for every block variation, and a plugin for every feature that each adds its own JavaScript and stylesheet. Out of the box, a typical WordPress site with a popular theme, a form plugin, an SEO plugin, and a slider is already shipping 2MB+ of assets before you add any actual content.
That doesn't mean WordPress is bad. It means you have to fight harder for performance on WordPress than on a headless setup. And if you're not aware of that going in, you'll end up with a 43 on Lighthouse wondering what happened.
The honest breakdown from what I've seen:
Headless CMS (Sanity, Contentful, Storyblok): You own the frontend. Performance is in your hands. The CMS doesn't inject anything into your build. If it's slow, it's your fault. That's actually a good thing.
WordPress: Can be fast, but requires intentional architecture. Block themes perform better than classic themes. Avoid heavy page builders if performance matters. Audit every plugin. Know what each one adds to the page weight.
Monolithic platforms (Drupal, AEM, Sitecore): Similar challenges to WordPress but at enterprise scale. More moving parts. More potential for bloat. Performance is achievable but requires dedicated effort and expertise.
Static site generators (Astro, Nuxt, Next.js in SSG mode): The fastest option by default because you're serving pre-built HTML. But the moment you start adding client-side interactivity and third-party scripts, you can still ruin it.
The point is not that one CMS is better than another. The point is that the CMS decision is a performance decision, and it should be evaluated as one.
The YouTube Embed Problem
Here's a concrete example that comes up constantly. A client site scoring poorly on Core Web Vitals. We investigate. The biggest offender? YouTube embeds.
A standard YouTube iframe loads around 800KB of JavaScript before the user even clicks play. That's one embed. Put four on a page and the browser is downloading over 3MB of scripts for videos most visitors will never watch.
LCP tanks. INP suffers because the main thread is busy parsing scripts nobody asked for. The page feels slow because it is slow.
The fix: don't load the iframe until the user wants to watch. Show a placeholder. Load on click. Reserve the space with aspect-ratio: 16/9 so there's no layout shift.
This is one of the things I built Boostify.js to solve. Instead of dropping a raw YouTube iframe in your markup, you set up a container and let Boostify handle the deferred loading:
<div
class="js--boostify-embed"
style="aspect-ratio: 16/9;"
data-url-youtube="https://www.youtube.com/embed/dQw4w9WgXcQ">
</div>
const bstf = new Boostify();
document.querySelectorAll('.js--boostify-embed').forEach(element => {
bstf.videoEmbed({
url: element.getAttribute('data-url-youtube'),
autoplay: true,
appendTo: element,
style: { height: "auto" }
});
});
Zero bytes of YouTube JavaScript loaded until the user interacts. The space is reserved so no layout shift. The page loads as if the video isn't there. When the user clicks, the embed loads instantly.
This needs to be a default in your component library, not a fix applied after someone complains about the Lighthouse score.
Third-Party Scripts: The Invisible Weight
YouTube is obvious because you can see it. But the worst offenders are invisible.
Google Analytics. Facebook Pixel. Hotjar. Microsoft Clarity. HubSpot tracking. LinkedIn Insight Tag. Cookie consent banners. Chat widgets. A/B testing tools.
On a typical site, I've counted 8 to 12 third-party scripts loading on every page. Each one competes for the main thread. Each one makes network requests. Each one adds weight.
None of these are critical for the user's first interaction. The user came to read content, check services, fill out a form. They didn't come so Hotjar could record their session before the page renders.
But these scripts are usually dropped straight into the <head>. Render-blocking. Loading before your actual content.
<!-- What I see on most sites when I open the source -->
<script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>
<script src="https://connect.facebook.net/en_US/fbevents.js"></script>
<script src="https://static.hotjar.com/c/hotjar-XXXXX.js"></script>
<!-- The page hasn't started rendering yet -->
With Boostify, you mark your tracking scripts with type="text/boostify" instead of type="text/javascript". The browser ignores them on load. Boostify picks them up and loads them only after the user interacts with the page (mouse move, scroll, touch).
<!-- Deferred until user interaction -->
<script type="text/boostify" src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>
const bstf = new Boostify();
bstf.onload({
worker: true,
callback: (result) => {
console.log('Third-party scripts loaded after user interaction');
}
});
Your content renders immediately. Analytics loads quietly in the background when the user is already engaged. You might lose tracking on 1-2% of ultra-fast bounces, but your site is faster, which typically increases conversions overall.
My recommendation: consolidate everything into a single Google Tag Manager container, mark that one script with type="text/boostify", and let GTM handle all your tags internally. One deferred script instead of twelve blocking ones.
Loading the Right Thing at the Right Time
The onload trigger handles the global stuff like analytics and tracking. But a real page has more than that. It has a slider halfway down. A contact form with a map at the bottom. An interactive widget in a section most users never scroll to.
This is where Boostify's scroll and observer triggers come in, and where things get interesting when you combine them.
Scroll trigger: fire a callback when the user scrolls past a certain distance. Good for loading resources that you know the user will need soon, like a slider component just below the fold.
bstf.scroll({
distance: 200,
callback: async () => {
// Load slider CSS and JS only when user starts scrolling
await bstf.loadStyle({
url: 'https://cdnjs.cloudflare.com/ajax/libs/tiny-slider/2.9.4/tiny-slider.css',
attributes: ['media=all'],
appendTo: 'head'
});
await bstf.loadScript({
url: 'https://cdnjs.cloudflare.com/ajax/libs/tiny-slider/2.9.2/min/tiny-slider.js',
attributes: ['type="text/javascript"'],
appendTo: 'body'
});
// Initialize once loaded
tns({
container: '.my-slider',
items: 3,
slideBy: 'page',
autoplay: true
});
}
});
Observer trigger: fire a callback when a specific element enters the viewport. This uses the Intersection Observer API under the hood. Perfect for content further down the page that may or may not be reached.
bstf.observer({
options: {
root: null,
rootMargin: '0px',
threshold: 0.01
},
element: document.querySelector('.contact-section'),
callback: async () => {
// Load the map embed only when the contact section is visible
bstf.videoEmbed({
url: 'https://www.google.com/maps/embed?pb=YOUR_MAP_EMBED',
appendTo: document.querySelector('.map-container'),
style: { height: '400px' }
});
}
});
The real power is using all three together. Think about a typical landing page:
const bstf = new Boostify();
// 1. Analytics and tracking: defer until first user interaction
bstf.onload({ worker: true });
// 2. Below-the-fold slider: load when user starts scrolling
bstf.scroll({
distance: 300,
callback: async () => {
await bstf.loadStyle({ url: '/css/slider.css', appendTo: 'head' });
await bstf.loadScript({ url: '/js/slider.js', appendTo: 'body' });
}
});
// 3. YouTube video in the middle: load when its section is visible
bstf.observer({
element: document.querySelector('.video-section'),
options: { threshold: 0.01 },
callback: () => {
bstf.videoEmbed({
url: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
autoplay: true,
appendTo: document.querySelector('.video-container'),
style: { height: 'auto' }
});
}
});
// 4. Contact form with map: load only if user reaches the bottom
bstf.observer({
element: document.querySelector('.contact-section'),
options: { threshold: 0.01 },
callback: async () => {
await bstf.loadScript({ url: '/js/form-validation.js', appendTo: 'body' });
await bstf.loadStyle({ url: '/css/form.css', appendTo: 'head' });
}
});
On initial page load, the browser downloads your HTML, your critical CSS, and your above-the-fold content. That's it. Everything else loads exactly when it's needed.
This isn't micro-optimization. On a page with a slider, two video embeds, a map, and eight tracking scripts, the difference between loading everything upfront and loading it progressively can be 4-5MB of JavaScript. That's the difference between a 2-second load and an 8-second load on mobile.
And it's an architecture decision. You set it up once in your component system. Every page benefits from it automatically.
CLS: Where Your CMS Shows Its True Colors
Cumulative Layout Shift exposes bad architecture more clearly than any other metric.
You know the experience. You're about to click a button and the page jumps because something loaded above it. A font swaps in and the layout shifts. An image loads without dimensions and pushes everything down.
This is where CMS choice really matters. A good CMS setup enforces constraints that prevent CLS. A bad one lets editors do whatever they want and hopes for the best.
If your CMS lets editors drop iframes anywhere without aspect ratio containers, you'll have CLS problems. If your theme doesn't enforce image dimensions, you'll have CLS problems. If your font loading strategy is "link to Google Fonts in the head," you'll have CLS problems.
The fix isn't a CSS hack after launch. It's building the system so these issues can't happen.
/* Defaults that should exist in every project */
img, video {
max-width: 100%;
height: auto;
}
.embed-container {
aspect-ratio: 16 / 9;
width: 100%;
}
On a headless setup, you control the component layer. You can enforce these patterns at the component level. On WordPress, you need to be more careful with theme defaults and block output. On page builder-heavy setups, good luck.
INP: The Metric That Exposes JavaScript Bloat
Interaction to Next Paint replaced First Input Delay in 2024. It's harder to pass because it measures the responsiveness of all interactions, not just the first one.
That hamburger menu that takes 400ms to respond? INP catches it. The accordion that freezes before opening? INP catches that too.
In 2026, pages at position 1 on Google show a 10% higher Core Web Vitals pass rate than those at position 9. Only 47% of sites reach Google's "good" thresholds across all three metrics. More than half the web is failing.
The most common INP killer is too much JavaScript on the main thread. Not just third-party scripts. First-party JavaScript too. Heavy page builders that inline JS for every component. Animations running on the main thread. Event listeners doing too much work synchronously.
This is another place where CMS architecture matters. A site built with a lightweight frontend framework on top of a headless CMS will naturally ship less JavaScript than a WordPress site running a page builder with 40 registered block types, each loading its own script.
And this is exactly why progressive loading matters. If you defer everything that's not immediately needed (using the onload, scroll, and observer pattern described above), the main thread is free to handle user interactions without competing for resources. INP improves because the browser isn't busy parsing tracking scripts and slider libraries while the user is trying to click things.
Performance Budgets at the Kickoff
I've never seen a creative brief that includes a performance budget. Not once across any project in any agency.
Nobody says "this page should load in under 2.5 seconds on a mid-range Android phone on 4G." But that's how Google evaluates your site. Mobile performance is the primary ranking signal. Even for desktop results.
53% of mobile visitors abandon a site that takes more than 3 seconds to load. A one-second delay reduces conversions by 7%. For a site generating $100K/month, that's $84K lost per year. For one second.
These numbers belong in every project kickoff. We try to set targets early:
- LCP under 2.5s on mobile
- INP under 200ms
- CLS under 0.1
- Total page weight under 1.5MB
- No more than 3 third-party scripts loading before first paint
These aren't aggressive. They're the thresholds Google considers "good." Most sites don't hit them because nobody set them as a goal.
Field Data vs Lab Data
One more thing. Stop obsessing over Lighthouse scores in isolation.
Lighthouse is lab data. It runs in a simulated environment. It's useful for diagnostics. But Google ranks you based on field data. Real users on real devices with real network conditions.
Your Lighthouse score might be 90 because you tested on your MacBook with fiber internet. But your field data in Search Console might show real users on mid-range phones getting an LCP of 4.2 seconds.
Field data is what matters. Check your Core Web Vitals report in Search Console. That's the real score.
The Takeaway
Performance is not a task at the end. It's not a sprint before launch. It's not a plugin.
It starts with the CMS you choose and how you set it up. It continues with how you handle scripts, images, fonts, and video. It lives in your component architecture, your loading strategy, your defaults.
Some platforms make this easier. Some make it harder. Neither is inherently wrong, but you need to know the tradeoffs before you commit.
By the time someone opens DevTools and says "why is this so slow," the answer is usually "because we never decided it should be fast."
Make that decision early. Make it part of the architecture. Hold every choice accountable to it.
Boostify.js is free and open source. All the loading patterns described in this article (deferred third-party scripts, scroll triggers, observer-based loading, video embed placeholders) are available out of the box. Check it out on GitHub.
Top comments (0)