Reducing INP on CMS Pages with Heavy Embeds

Within Core Web Vitals Optimization, this guide targets a common source of poor Interaction to Next Paint on content sites: third-party embeds placed by editors. Video players, social posts, maps, podcast players and forms each bring their own JavaScript, and a single article can carry several. The guide shows how to render lightweight facades, load the real embed only on interaction, keep embed scripts off the main thread where possible, and limit embeds in the content model.

Embeds are hard to control because they are content, not code. Developers build a clean article template; editors then add a YouTube video, two social posts and a map, each of which loads hundreds of kilobytes of script, registers event listeners and runs work on the main thread during and after load. When a reader taps a menu or a button while those scripts initialize, the tap waits. INP reflects the worst of these waits, so one heavy embed can make a whole template fail.

Facade first, player on demandThe page renders a static facade with a poster image and play button from CMS data; nothing third-party loads until the reader clicks, at which point the facade swaps in the real player, which starts immediately.ReaderPageVideo providerrender facade:poster + play buttonclick playload player script + iframeplayer readyvideo plays
Readers who never play the video never pay for the player.

The Problem

A news publisher’s article template had a p75 INP of 410 milliseconds on mobile. Articles averaged 2.3 embeds, mostly social posts and videos. Performance traces showed long tasks from social widget scripts during the first ten seconds after load, exactly when readers scrolled and tapped the navigation. Articles without embeds had an INP of 140 milliseconds. The editorial team would not give up embeds, which were central to its coverage.

How to Tame Embeds

Facades. Render a static preview, built from data stored in the CMS: a poster image and title for videos, the text and author of a social post, a static map image for maps. The facade is plain HTML and CSS with no third-party script. On click, replace it with the real embed.

Load on interaction or visibility. For embeds that must be live, such as forms, load the script only when the embed scrolls near the viewport, and never before the main content is interactive. Use requestIdleCallback or a short timeout after load to avoid competing with the first interactions.

Isolate. Embeds inside iframes run their scripts in a separate document, but same-process iframes still compete for the main thread on many devices. Cross-origin iframes help, and sandboxing prevents embeds from injecting work into your page.

Limit in the model. Give embed blocks their own type in the CMS, cap how many can appear per page, and require the data needed for a facade, such as a poster image and a title, when the embed is added.

Strategy per embed kindFor videos, social posts, maps, forms and podcast players, the facade content, when to load the real embed, and what the CMS must store.EmbedFacadeLoad real embedCMS storesVideoposter + play buttonon clickvideo id, poster, titleSocial posttext, author, dateon click or neverpost URL, snapshot textMapstatic map imageon clickcoordinates, zoomFormstyled placeholdernear viewport, after idleform idPodcastcover + play buttonon clickepisode URL, cover
Facades cover most embeds; live embeds load late and near the viewport.

Implementation

The embed block renders a facade and swaps in the real embed on click. The component is a small client island; the rest of the article stays server-rendered.

TSX
// components/blocks/video-embed.tsx
"use client";
import { useState } from "react";

export function VideoEmbed({ videoId, title, poster, width, height }: { videoId: string; title: string; poster: string; width: number; height: number }) {
  const [active, setActive] = useState(false);
  const ratio = { aspectRatio: `${width} / ${height}` };

  if (active) {
    return (
      <iframe
        style={ratio}
        className="embed"
        src={`https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}?autoplay=1`}
        title={title}
        allow="autoplay; encrypted-media; picture-in-picture"
        allowFullScreen
        loading="eager"
      />
    );
  }
  return (
    <button type="button" className="embed embed--facade" style={ratio} onClick={() => setActive(true)} aria-label={`Play video: ${title}`}>
      <img src={poster} alt="" width={width} height={height} loading="lazy" decoding="async" />
      <span className="embed__play" aria-hidden="true" />
    </button>
  );
}

For embeds that load when visible, an intersection observer triggers loading once the element is near the viewport and the browser is idle.

TypeScript
// lib/load-when-near.ts
export function loadWhenNear(el: Element, load: () => void, rootMargin = "600px") {
  const io = new IntersectionObserver((entries) => {
    if (entries.some((e) => e.isIntersecting)) {
      io.disconnect();
      const idle = (window as { requestIdleCallback?: (cb: () => void) => void }).requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 200));
      idle(load);
    }
  }, { rootMargin });
  io.observe(el);
}

For social posts, store a snapshot of the text, author and date in the CMS when the editor adds the embed, using the provider’s oEmbed endpoint. The facade then shows real content without any third-party script, and a “View on the platform” link replaces the live widget for most readers. The live widget can still load on click for those who want it.

Enforcing limits in the content model

Model embeds as blocks with required facade fields and a validation rule limiting their number per page. When an editor pastes a video URL, a CMS extension or webhook can fetch the poster and title automatically, so the facade data costs editors nothing. Rich text fields should not allow raw embed HTML, which bypasses all of this; offer embed blocks inside rich text instead.

Keeping your own components light

Embeds are the most visible cause, but the page’s own islands also contribute. Each client component hydrates, attaches listeners and may run effects on load. Audit the article template’s islands: share buttons, table-of-contents highlighters, newsletter forms and comment counters are often client components that could be server-rendered with a small script, or loaded only when visible. Handlers should do little work synchronously: update the visible state first and defer analytics and non-urgent work with a yield to the main thread, so the next paint happens quickly after the tap.

Configuration Reference

Setting Recommendation Why
Default rendering facade from CMS data No third-party script until needed.
Video, map, podcast load on click Most readers never interact.
Forms load near viewport after idle Ready before the reader reaches them.
Social posts stored snapshot, widget on click Real content without widget scripts.
Embeds per page limit in validation, for example 4 Keeps worst cases bounded.
Raw HTML embeds not allowed in rich text Prevents bypassing facades.

Gotchas & Edge Cases

  • Autoplay after click. Browsers allow autoplay after a user gesture, but some providers need an extra parameter. Test that one click starts playback, or readers must click twice.
  • Accessibility of facades. Facades are buttons: give them an accessible name that says what will play, and keep keyboard focus on the player after it loads.
  • Privacy and consent. Facades also avoid loading third-party trackers before consent. Load the real embed only after consent where required, and explain that in the facade.
  • Layout shift on swap. The real embed must occupy exactly the facade’s box. Use the same aspect ratio for both.

Worked Example

The news publisher introduced facades for videos, podcasts and maps, stored snapshots for social posts with the live widget on click, and a limit of five embeds per article. Rich text lost its raw HTML option in favour of embed blocks. Mobile p75 INP on the article template fell from 410 to 170 milliseconds in the next field window, and total JavaScript on a typical article dropped by about 900 kilobytes. Video plays per article did not decrease, and social embed clicks showed that most readers had never interacted with the widgets anyway.

Article template, mobile p75 INPMobile p75 INP for articles before and after facades and embed limits, compared with articles that have no embeds at all.With embeds, before410 msWith embeds, after170 msNo embeds (reference)140 ms
Articles with embeds now perform close to articles without them.

Finding the Worst Offenders

Not every embed type costs the same, and it pays to measure before converting all of them. Use INP attribution from real-user monitoring, as described in measuring Core Web Vitals per content type, to see which block types are responsible for the slowest interactions, and record lab traces of articles with each embed type to measure main-thread time and script size. Social widgets and ad-supported video players are usually the heaviest; simple iframes such as podcast players are lighter. Convert the heaviest first, and keep the measurement running, because providers change their scripts without notice and a harmless embed can become a heavy one after a provider update.

Rollout Checklist

  • Model embeds as blocks with required facade data.
  • Render facades by default and load the real embed on click.
  • Load live embeds near the viewport after the page is idle.
  • Replace social widgets with stored snapshots and a link.
  • Limit embeds per page and remove raw HTML from rich text.
  • Measure INP by block type and revisit when providers change.

Frequently Asked Questions

Do facades hurt engagement with videos?

Rarely. Readers who want to watch click once, as they would anyway. Measure plays before and after to be sure.

Is lazy-loading the iframe enough?

It helps LCP and data usage but not INP once the iframe is near the viewport, because the player’s scripts still run. Facades avoid the scripts entirely until needed.

How do we handle embeds in existing content?

Migrate them: parse raw embed HTML in rich text, create embed blocks with facade data from oEmbed, and replace the HTML in a scripted migration.

Should facades be used on desktop too?

Yes. Desktop devices handle scripts better, but facades still save bandwidth and avoid trackers before consent.

Can web workers run third-party embeds?

Tools that proxy third-party scripts to workers exist, but compatibility varies. Facades are simpler and more reliable for most embeds.