
Every YouTube or Vimeo embed you add to your Framer site pulls in over 500KB of player resources—even when visitors never hit play. Drop three videos onto a page and you've silently added 1.5MB+ of bloat that drags down your entire site.
This guide walks you through two methods to eliminate that problem: a quick 2-minute patch using native browser lazy loading, and a proper solution using our brixLazyLoadVideo Code Override that delivers the best possible performance. The brixLazyLoadVideo override is free forever and takes 10 minutes or less to set up.

When you drop a YouTube or Vimeo embed into your Framer page, it doesn't just fetch the video file. It loads the complete player infrastructure: JavaScript bundles, CSS files, fonts, tracking scripts, and preview thumbnails. A single YouTube embed brings in roughly 500-800KB of data that your visitor's browser has to download before the page feels responsive.
This happens the moment the page loads—even when the video sits below the fold, even when the visitor never scrolls to it, even when they have no intention of watching. When you stack multiple videos on a single page, the damage multiplies fast. Three embeds can pile on 1.5MB+ of unnecessary overhead. Ten videos on a portfolio page? You're looking at potentially 5MB of dead weight before your actual content even renders.
Lazy loading fixes this by postponing video resources until they're genuinely needed. Rather than loading heavy embeds immediately, lazy loading holds off until the video enters the viewport or until the user clicks play. The payoff is dramatically faster initial page loads, smoother user experience, and better performance across your entire Framer site.
There are two methods to lazy load videos in Framer, and each comes with different trade-offs.
Native lazy loading leverages the browser's built-in loading="lazy" attribute on iframes. You apply a small Code Override that patches this attribute onto your embeds, and the browser automatically postpones loading until the video approaches the viewport. It's dead simple—just two minutes to implement.
But it has real limitations. The browser still fetches the full 500KB+ player once triggered; you're just pushing back when that happens. You also surrender control over the exact timing—browsers decide based on scroll position and network conditions.
Click-to-load (using brixLazyLoadVideo) works completely differently. Instead of an iframe, visitors see a lightweight thumbnail image with a play button overlay. The actual video player only loads when someone clicks play.
This means zero video resources load unless a visitor explicitly chooses to watch. For a page with five videos, native lazy loading might defer 2MB initially but still loads everything as users scroll. Click-to-load keeps that 2MB off the page permanently unless users actually engage with the videos.
When to use each method: Go with native lazy loading if you have just 1-3 videos below the fold and want the quickest possible fix. Use click-to-load for landing pages, portfolios with multiple videos, pages where performance matters, or any video visible above the fold (where native lazy loading does nothing).
This quick method adds browser-native lazy loading to your video embeds through a Code Override. It's the fastest fix but delivers moderate performance gains compared to the click-to-load approach.
When you use Framer's built-in YouTube, Vimeo, or Embed components, you don't have direct access to the iframe markup. The embed gets generated automatically, and there's no setting to enable lazy loading.
The workaround is straightforward: create a Code Override that patches the loading="lazy" attribute onto any iframe inside the target layer after it renders.
Follow these steps to add native lazy loading to your Framer embeds:


Paste this Code Override into the file:
import { forwardRef, useEffect, useRef, type ComponentType } from "react"
/*!
* BRIX Templates Lazy Iframe for Framer
* ----------------------------------------------------------------------------
* Patches any <iframe> inside the target layer to add loading="lazy".
* Uses browser-native lazy loading to defer iframe resources.
*
* Usage: Apply this override to any Frame/Stack containing an embed.
*
* Version: 1.0.0
* Author: BRIX Templates
*/
export function brixLazyIframe(Component): ComponentType {
return forwardRef((props: any, ref: any) => {
const hostRef = useRef<HTMLElement | null>(null)
//--------------------------------------------------------------------------
// Ref Handling
//--------------------------------------------------------------------------
/**
* Merges Framer's forwarded ref with our local ref
* This ensures effects and links keep working properly
*/
const setRef = (node: HTMLElement | null) => {
hostRef.current = node
if (typeof ref === "function") ref(node)
else if (ref && "current" in ref) ref.current = node
}
//--------------------------------------------------------------------------
// Iframe Patching
//--------------------------------------------------------------------------
useEffect(() => {
const host = hostRef.current
if (!host) return
/**
* Finds all iframes and adds lazy loading attribute
*/
const patchIframes = () => {
const iframes = host.querySelectorAll("iframe")
iframes.forEach((iframe) => {
// Browser-native lazy loading
iframe.setAttribute("loading", "lazy")
// Optional: hint to browser this is low priority
iframe.setAttribute("fetchpriority", "low")
})
if (iframes.length > 0) {
console.log("🎬 BRIX Lazy Iframe: Patched " + iframes.length + " iframe(s)")
}
}
// Patch immediately
patchIframes()
// Some embeds inject the iframe after mount, so observe for changes
const observer = new MutationObserver(patchIframes)
observer.observe(host, { childList: true, subtree: true })
return () => observer.disconnect()
}, [])
return <Component ref={setRef} {...props} />
})
}The loading="lazy" attribute tells the browser to postpone loading until the iframe approaches the viewport.

While native lazy loading is fast to implement, you should understand its drawbacks:
For pages with multiple videos or where performance is critical, the click-to-load method below delivers substantially better results.
This method delivers maximum performance gains by loading absolutely nothing until the user clicks play. Visitors see a fast-loading thumbnail image with a play button overlay, and the actual video player only loads on interaction.
The click-to-load approach offers significant advantages over native lazy loading:
Since Code Overrides in Framer can't have Property Controls (those are reserved for Code Components), you need a way for designers to configure each video without touching code.
The cleanest solution: store the video URL in the layer's Link field. The override reads the link, extracts the video ID, and handles everything else automatically.
This approach scales beautifully for:
First, add the click-to-load override to your project. This is a one-time setup that you can reuse across your entire site.
Go to Assets → Code → New Override (or open your existing overrides file) and add this code:
import { forwardRef, useEffect, useMemo, useState, type ComponentType } from "react"
/*!
* BRIX Templates Lazy Load Video for Framer
* ----------------------------------------------------------------------------
* Loads YouTube/Vimeo videos only when users click play.
* Uses lightweight thumbnails until interaction for maximum performance.
*
* Usage:
* 1. Design your thumbnail (Image + play button) in a Frame
* 2. Set the Frame's Link to your YouTube/Vimeo URL
* 3. Apply this override to the Frame
*
* Supported URL formats:
* - https://www.youtube.com/watch?v=VIDEO_ID
* - https://youtu.be/VIDEO_ID
* - https://vimeo.com/VIDEO_ID
* - youtube:VIDEO_ID (compact format)
* - vimeo:VIDEO_ID (compact format)
*
* Version: 1.0.1
* Author: BRIX Templates
*/
//--------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------
type Provider = "youtube" | "vimeo"
type ParsedVideo = { provider: Provider; id: string } | null
//--------------------------------------------------------------------------
// Utility Functions
//--------------------------------------------------------------------------
/**
* Detects if user is on a mobile device
* Used to determine if videos should be muted for autoplay compatibility
*/
function isMobileDevice(): boolean {
if (typeof window === "undefined") return false
const maxWidth = 767
return (
window.innerWidth <= maxWidth ||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
)
}
//--------------------------------------------------------------------------
// URL Parsing
//--------------------------------------------------------------------------
/**
* Extracts provider and video ID from various URL formats
*/
function parseVideoUrl(raw: string): ParsedVideo {
if (!raw) return null
// Support compact "provider:id" format (youtube:dQw4w9WgXcQ)
const compact = raw.match(/^(youtube|vimeo)\s*:\s*([a-zA-Z0-9_-]+)$/i)
if (compact) {
return { provider: compact[1].toLowerCase() as Provider, id: compact[2] }
}
let url: URL
try {
url = new URL(raw)
} catch {
return null
}
const host = url.hostname.replace(/^www\./, "")
const pathParts = url.pathname.split("/").filter(Boolean)
// --- YouTube ---
if (host === "youtu.be") {
const id = pathParts[0]
return id ? { provider: "youtube", id } : null
}
if (host.endsWith("youtube.com") || host === "youtube-nocookie.com") {
// https://youtube.com/watch?v=ID
if (url.pathname === "/watch") {
const id = url.searchParams.get("v")
return id ? { provider: "youtube", id } : null
}
// https://youtube.com/embed/ID
if (pathParts[0] === "embed" && pathParts[1]) {
return { provider: "youtube", id: pathParts[1] }
}
}
// --- Vimeo ---
if (host.endsWith("vimeo.com")) {
// https://vimeo.com/507360544
const maybeId = pathParts[0]
if (maybeId && /^\d+$/.test(maybeId)) {
return { provider: "vimeo", id: maybeId }
}
// https://player.vimeo.com/video/507360544
if (pathParts[0] === "video" && pathParts[1] && /^\d+$/.test(pathParts[1])) {
return { provider: "vimeo", id: pathParts[1] }
}
}
if (host === "player.vimeo.com" && pathParts[0] === "video" && pathParts[1]) {
return /^\d+$/.test(pathParts[1]) ? { provider: "vimeo", id: pathParts[1] } : null
}
return null
}
//--------------------------------------------------------------------------
// Embed URL Builder
//--------------------------------------------------------------------------
/**
* Builds the iframe src URL with autoplay parameters
* Automatically mutes on mobile for autoplay compatibility
*/
function buildEmbedSrc(video: { provider: Provider; id: string }): string {
const shouldMute = isMobileDevice()
if (video.provider === "youtube") {
const params = new URLSearchParams({
autoplay: "1",
playsinline: "1",
rel: "0",
modestbranding: "1",
})
if (shouldMute) params.set("mute", "1")
return `https://www.youtube.com/embed/${video.id}?${params.toString()}`
}
// Vimeo
const params = new URLSearchParams({
autoplay: "1",
title: "0",
byline: "0",
portrait: "0",
})
if (shouldMute) params.set("muted", "1")
return `https://player.vimeo.com/video/${video.id}?${params.toString()}`
}
//--------------------------------------------------------------------------
// Override
//--------------------------------------------------------------------------
export function brixLazyLoadVideo(Component): ComponentType {
return forwardRef((props: any, ref: any) => {
// Framer layers with links expose href
// Check common fallbacks to be safe
const href: string =
props?.href ?? props?.link ?? props?.url ?? props?.videoUrl ?? ""
const video = useMemo(() => parseVideoUrl(href), [href])
const [isLoaded, setIsLoaded] = useState(false)
// Log when video is ready
useEffect(() => {
if (video) {
console.log("🎬 BRIX Lazy Load Video: Ready - " + video.provider + " (" + video.id + ")")
}
}, [video])
/**
* Handles click/tap events
* Prevents navigation and loads the video player
*/
const onTap = (event?: any) => {
// If this is an <a>, stop navigation
event?.preventDefault?.()
if (!video) {
// No valid video URL found, let normal behavior happen
props?.onTap?.(event)
props?.onClick?.(event)
return
}
// Load video on first click
if (!isLoaded) {
setIsLoaded(true)
console.log("🎬 BRIX Lazy Load Video: Playing - " + video.provider + " (" + video.id + ")")
}
// Preserve any existing handlers
props?.onTap?.(event)
props?.onClick?.(event)
}
// If URL isn't parseable, attach handler but do nothing special
if (!video) {
return <Component ref={ref} {...props} onTap={onTap} />
}
const iframeSrc = buildEmbedSrc(video)
return (
<Component
ref={ref}
{...props}
onTap={onTap}
style={{
...props?.style,
cursor: isLoaded ? props?.style?.cursor : "pointer",
}}
>
{props.children}
{isLoaded && (
<iframe
key={`${video.provider}:${video.id}`}
src={iframeSrc}
title="Video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
loading="eager"
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
border: 0,
}}
/>
)}
</Component>
)
})
}Save the file. The override is now available across your project.
With the override saved, adding lazy-loaded videos is straightforward. Each video just needs a thumbnail and a link.

The override accepts multiple URL formats:
YouTube:
Vimeo:
The compact format is handy when you want to keep links short, especially in CMS setups.
The YouTube video ID is the code after v= in any YouTube URL.
For this URL: https://www.youtube.com/watch?v=dQw4w9WgXcQ
The video ID is: dQw4w9WgXcQ
The Vimeo video ID is the number after vimeo.com/ in any Vimeo URL.
For this URL: https://vimeo.com/507360544
The video ID is: 507360544
You need a thumbnail image for each video. Here are the easiest ways to get them:
For YouTube videos, use this URL pattern to grab the default thumbnail:
https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg
Replace VIDEO_ID with your actual video ID. Download this image and upload it to your Framer project.
For Vimeo videos, the thumbnail API is limited, so the easiest option is to take a screenshot from the video or upload your own custom thumbnail.
You can also use any custom thumbnail you prefer for either platform—it doesn't have to be the default video thumbnail. Just upload your image and use it inside your video wrapper Frame.
The click-to-load pattern integrates seamlessly with Framer CMS for dynamic video galleries or portfolio pages.
First, create these fields in your CMS Collection:
Then, on your Collection page or Collection list:
Now every CMS item automatically gets lazy-loaded video functionality without any additional code.
After publishing your site, verify that lazy loading works correctly with these checks.
Lazy loading is a performance technique that postpones loading video resources until they're genuinely needed. Instead of loading heavy YouTube or Vimeo embeds the moment your page loads, lazy loading waits until the video enters the viewport or until the user clicks play.
This dramatically cuts initial page load time because video embeds typically add 500KB+ of scripts and resources. In Framer, you can implement lazy loading using a Code Override that patches the native loading="lazy" attribute onto iframes, or more effectively using a click-to-load pattern that displays a thumbnail until user interaction. The click-to-load approach delivers the best performance because it loads zero video resources until someone actually wants to watch.
Every YouTube or Vimeo embed fetches the entire video player infrastructure immediately—JavaScript bundles, CSS files, fonts, tracking scripts, and preview thumbnails. A single YouTube embed adds roughly 500-800KB of data that your visitor's browser must download before the page feels responsive.
This happens even when the video sits below the fold or when the visitor never plans to watch it. When you have multiple videos on a page, this stacks up quickly: three embeds can add 1.5MB+ of unnecessary initial load. The solution is implementing lazy loading so these resources only load when needed, as covered in Method 2 of this guide.
Native lazy loading using loading="lazy" tells the browser to postpone loading an iframe until it approaches the viewport. It's simple to implement but the browser still fetches the full video player once triggered—you're just pushing back when that happens.
Click-to-load (facade pattern) displays a static thumbnail image with a play button. The actual video player never loads unless someone clicks. This means zero video resources on initial page load, regardless of scroll position. For a page with five videos, native lazy loading might defer 2MB initially but still loads everything as users scroll. Click-to-load keeps that 2MB off the page permanently unless users actually engage with videos—making it the superior choice for most situations.
Code Overrides in Framer are small React Higher-Order Components you attach to any layer. They let you modify props, events, styles, and behavior at render time. For video lazy loading, overrides can patch iframe attributes or intercept click events to inject video players on demand.
One important detail: overrides only run in Preview and Published mode—they don't execute on the Framer canvas. This is actually perfect for lazy loading because you want to test real performance behavior anyway. The brixLazyLoadVideo override reads the video URL from the layer's Link field, making it designer-friendly since no code changes are needed per video.
Lazy loading videos actually helps SEO rather than hurting it. Search engines favor fast-loading pages, and cutting initial page weight by lazy loading videos directly improves load speed metrics.
Google's crawlers are sophisticated enough to understand lazy-loaded content. The key is ensuring your page still has relevant text content and that video thumbnails include descriptive alt text for accessibility and indexing. For click-to-load implementations, the thumbnail images provide visual context that search engines can index. Faster pages translate to better user experience metrics, lower bounce rates, and ultimately better search rankings.
Yes, both lazy loading methods work with Framer CMS. For native lazy loading, apply the brixLazyIframe override to any Frame containing a CMS-bound embed.
For click-to-load, create CMS fields for Video URL and Video Thumbnail, then bind these to the Frame's Link property and the Image source. The brixLazyLoadVideo override automatically initializes all videos on the page, including those generated dynamically from CMS collections. This makes it perfect for video portfolios, course platforms, or any site with multiple dynamic videos.
Yes, both the YouTube and Vimeo embeds are configured with autoplay parameters. When a user clicks the thumbnail, the video player loads and begins playing immediately.
On mobile devices, browsers have strict autoplay policies that may require videos to be muted for automatic playback. The iframe parameters include playsinline for mobile compatibility. If autoplay doesn't work on a specific device, users can simply tap the player controls to start playback. This is a browser-level restriction, not a limitation of the lazy loading implementation.
There's no practical limit to how many videos you can lazy load on a page. In fact, more videos makes lazy loading more valuable.
Without lazy loading, a page with 10 video embeds would need to fetch 5MB+ of resources upfront. With click-to-load lazy loading, that same page loads with zero video overhead until users interact. Each video loads independently when clicked, so even with 50 videos on a page, only the ones users actually watch consume resources. This makes click-to-load ideal for video galleries, course libraries, or any page with many videos.
The brixLazyIframe override works with any iframe-based embed since it simply patches the loading attribute. This includes YouTube, Vimeo, Wistia, Loom, and others.
The brixLazyLoadVideo override specifically supports YouTube and Vimeo, which cover the vast majority of use cases. The URL parser handles multiple formats for both platforms. For other providers, you could extend the override by adding new parsing logic, or use the native lazy loading approach with any iframe embed.
Yes, but only with the click-to-load method. Native loading="lazy" has no effect on above-the-fold content because the browser loads anything visible in the initial viewport immediately.
However, click-to-load works regardless of position because it replaces the video with a thumbnail until user interaction. For hero videos or any prominent video visible on page load, click-to-load is the only way to avoid the performance hit. The thumbnail loads instantly, the page renders fast, and the actual video player only loads when the user demonstrates intent by clicking play.
Lazy loading external videos is one of the highest-impact performance optimizations for Framer sites. The click-to-load approach using brixLazyLoadVideo delivers maximum performance gains by loading zero video resources until users actually want to watch. For simpler implementations, native loading="lazy" via brixLazyIframe provides a quick fix with moderate benefits.
Start with the click-to-load method for any page where performance matters—especially landing pages, portfolios with multiple videos, or CMS-driven video galleries. The one-time override setup pays dividends across your entire site.
For advanced implementations like custom video players, analytics integration, or complex CMS setups, our Framer development team can create tailored solutions that align with your specific requirements.

Learn two ways to lazy load YouTube and Vimeo in Webflow: quick native fix plus click-to-load pattern for better performance.

Step-by-step guide to implementing work-email-only validation in Framer forms. Block personal providers and improve lead quality instantly.

Track Framer button clicks with Google Tag Manager and send detailed GA4 events—no code required. Complete step-by-step guide.