Free · Craft CMS 5
The page moved as you read
An embed arrives in a page as somebody else's markup and behaves like it. It loads whatever it wants whenever it wants, and it settles to a height nobody knew in advance — after the reader has already started reading. Bed lays a bed for each one: space reserved from what the embed actually measured on real page views, loading deferred until it is wanted, and connections warmed only for the ones that are wanted immediately.
It happens on the way out the door
No field, no shortcode, no template change. Bed rewrites the finished response, so it does not care whether the embed came from a rich-text field, a hard-coded template or another plugin's render.
<!-- what you pasted -->
<iframe src="https://player.vimeo.com/video/76979871"
width="640" height="360"></iframe>
<!-- what goes out -->
<div class="bed bed--vimeo bed--iframe bed--ratio"
data-bed="e87d290a…" style="--bed-ar:640/360">
<iframe src="https://player.vimeo.com/video/76979871"
width="640" height="360" loading="lazy"
title="Vimeo embed"
referrerpolicy="strict-origin-when-cross-origin"></iframe>
</div>
<!-- and for the ones whose height nobody can know in advance, later: -->
<style>
[data-bed="2a8cbce…"]{--bed-min:550px}
@media (min-width:1025px) and (max-width:1280px){
[data-bed="2a8cbce…"]{--bed-min:623px}
}
</style>
Features
Four things that make an embed behave, and two that make the first four honest.
Measured, not guessed
A guess that is too big leaves a hole and a guess that is too small still moves the page. So a sampled fraction of visitors beacon back what each embed settled to at their own viewport width, and every later render reserves that.
- Seven viewport buckets, emitted as a media-query ladder
- Widths nobody has visited stay unreserved rather than guessed
Known shapes need no measurement
A YouTube frame, or anything carrying width and height, gets an aspect-ratio box on its very first render. Recognition buys the natural ratio for free — there is nothing to wait for.
- Around sixty providers, each with its own natural ratio
- An unrecognised embed still gets a bed from its own dimensions
Lazy, except at the top
Frames get loading="lazy" and media gets preload="none" — but the first embed on the page, and anything measured inside the first screenful, is left eager.
- Above the fold at any width counts as above the fold everywhere
- Lazy-loading the LCP element is the one mistake that actually costs
Loader scripts lifted out
A tweet, a TikTok, an Instagram post — the provider's loader script is lifted out of the document entirely and injected by an IntersectionObserver when the embed nears the viewport.
- Root margin is a setting, so it starts before the reader arrives
- Bed checks the runtime is really on the page before it lifts anything
A facade in front of the player
A video player is a whole document, a few hundred kilobytes of script and a handful of third-party connections, all spent before anybody has decided to watch anything. Bed puts a poster and a play button there instead.
- The real player swaps in on the click, with autoplay
- Turn posters off and a reader who never clicks makes no third-party request at all
Warm connections, on a budget
Preconnect for the providers whose embeds load immediately, dns-prefetch for the rest — deduped and capped, because a preconnect is a socket, a DNS lookup and a TLS handshake opened on spec.
- Six per page by default: a budget, and never a target
- A provider measured above the fold is upgraded to preconnect automatically
Frequently Asked Questions
The questions worth answering before you install it.
Entirely, permanently, with no editions and no licence key. Every feature on this page is in the one version there is, and there is no licensing code in the plugin to remove later.
No. Bed makes no outbound HTTP request anywhere — not at install, not on a render, not on a schedule. The provider registry is a static description of what embeds look like, not a client for talking to anybody, and facade posters are derived from the embed URL rather than fetched. The only traffic Bed adds is a beacon from your own visitors to your own site.
Nothing that identifies one. A row is an aggregate — a count of samples, a sum of heights, the tallest seen, and a tally of how often the embed started above the fold, per slot and per viewport bucket. No address, no user agent, no session, no cookie, and no URL taken from the client.
It has to be public — it is called by sendBeacon from a page that may have come out of a full-page cache hours ago, when no session can be assumed. What makes it safe is the token: signed with Craft's own security key, naming the site, the page and the exact list of slots, so a client can only report on embeds it was actually served. Every number is clamped and every width is snapped to a bucket on the server, and there is a per-address rate limit and a hard row cap on top.
No, and deliberately so. Rendering never writes — a filter that inserts a row per page view turns every cache miss into a write. Slot rows are created by the collector endpoint; the renderer's one question is answered by a cached read.
Yes. The rewrite happens on the way out of Craft, so it is baked into whatever gets cached, and cached pages carry their signed token with them — which is why the token's default lifetime is seven days rather than a session's worth. A page cached before a slot was measured keeps its unreserved bed until the cache is refreshed.
Immediately for anything with a knowable shape. For the rest, one page view in ten is sampled and a slot wants twenty samples per viewport bucket, so roughly two hundred views of a page fills one bucket. Turn the sample rate up to 1 in development to watch it happen in an afternoon.
The rewrite costs about 0.3 ms on a 33 KB page, and a page with no embeds on it is rejected by a single pattern match before any of that runs. The stylesheet is about a kilobyte and is inlined rather than linked — putting the thing whose job is to hold space before the first paint behind a render-blocking request would be a performance plugin causing the problem it measures.
No. There is no field type, no picker, no reference tag and no element. Bed takes HTML that already has embeds in it — from a rich-text field, a hard-coded template, or another plugin's render — and makes them behave on the way out the door. If you want to author embeds in Craft, that is Eye. The two compose.
Nothing. Bed never modifies stored content — the wrappers are added to the response, so what is in the database is the markup you pasted in. Uninstalling drops the two measurement tables and every page goes back to exactly the embeds it had before.
Craft CMS 5.3+ and PHP 8.2+, with no runtime dependencies beyond Craft's own and no build step.
Free, and it stays free
No editions, no licence key, no trial. Install it, and the next page you render has a bed under every embed on it.