Why your Framer Website is slow on mobile — and how we fixed it

Why your Framer Website is slow on mobile — and how we fixed it

Framer website running slow on mobile? Here's how we fixed video crashes, lazy loading, and JS bloat for a client site — with the exact code we used.

Case Study

Tutorial

Web Development

Framer

Mobile

Performance

Why Your Framer Website Is Slow on Mobile — And How We Fixed It

A slow Framer website on mobile usually comes down to four things: unoptimized videos, media loading before it's needed, over-reliance on third-party renderers, and overlooked code in your own components. In a recent client project, fixing all four eliminated crashes on older phones, dramatically reduced total page weight, and made the site feel instant on any device. Here's exactly what we changed and why.

We recently worked on a client's Framer website that ran perfectly well on most machines but felt sluggish on older hardware. New phone? No issues. Older hardware was a different story. Some pages stuttered and every now and then the browser would just give up and crash.

The awkward thing about this kind of work is that nobody notices when it goes right. A page that loads instantly doesn't get a second thought. The one that hangs for a moment is the one people remember. So we figured it was worth writing down what we changed and why, because the reasons turned out to be insightful enough to share.

Why Videos Crash Framer Sites on Mobile (And How to Fix Them)

Most of the trouble traced back to one thing: videos. The clips had been converted to a newer web format (.webm) that looks great right up until someone opens the site in an older version of Safari on an iPhone, where it isn't fully supported. For some visitors, the players just came up blank or would crash the page entirely.

The videos were also rendered at 120 frames per second. When a few of them tried to play simultaneously, it was more than older phones could handle — that's what was behind the crashes. The files were a full 1920×1080, even though nowhere on the page did they actually display near that size. Every visitor was pulling down far more pixels and data than they would ever need.

So we re-encoded them. We switched back to .mp4, which has far wider support across all browsers and devices. We applied stronger compression, brought the frame rate down from 120 to 30 (imperceptible to viewers, significant to the browser), and dropped the resolution to 1280×720 to match the size they were actually displayed at. One command handled all of it:

ffmpeg -i big.mp4 -vf "scale=1280:-2,fps=30" -c:v libx264 -preset slow -crf 23 -c:a aac -b
ffmpeg -i big.mp4 -vf "scale=1280:-2,fps=30" -c:v libx264 -preset slow -crf 23 -c:a aac -b
ffmpeg -i big.mp4 -vf "scale=1280:-2,fps=30" -c:v libx264 -preset slow -crf 23 -c:a aac -b

The clips now play everywhere, weigh a fraction of what they used to, and stopped overwhelming phones.

The rule of thumb: match your video resolution to its display size, cap frame rates at 30fps for background/decorative video, and always use .mp4 as your primary format with .webm as a progressive enhancement for browsers that support it.

How to Lazy Load Images and Videos in Framer

A browser doesn't have to grab everything the second a page opens — but by default, Framer gives it very little guidance on what to prioritise.

Prioritise what's above the fold. The large images at the top of the page are the first thing anyone sees, so we told the browser to treat them as a priority with one attribute:

<img src="hero.jpg" fetchpriority="high" alt="…">
<img src="hero.jpg" fetchpriority="high" alt="…">
<img src="hero.jpg" fetchpriority="high" alt="…">

Since we were working in Framer, we created a simple code override and applied it to those image elements. This tells the browser to fetch these assets before anything else, which directly improves LCP (Largest Contentful Paint) — the metric Google uses to measure how quickly the main content loads.

Delay everything else. Further down the page, plenty of media was loading immediately even though most visitors would never scroll far enough to see it. The worst offender was a custom before-and-after comparison slider that appears in several places on the page. Each instance renders two pieces of media side by side. With several of these components sitting below the fold, every page load was downloading and processing media that most visitors would never reach.

Native lazy loading (loading="lazy") wasn't an option here, because the component renders its media as CSS backgrounds and mounted video elements rather than plain <img> tags. So instead we wrote a small observer that watches each component's position and only loads its media once it comes close to entering the viewport. The Intersection Observer API is built for exactly this:

const [mediaVisible, setMediaVisible] = useState(false)
const containerRef = useRef(null)

useEffect(() => {
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        setMediaVisible(true); // load the images / mount the videos
        observer.disconnect(); // only needs to happen once
      }
    },
    { rootMargin: "100px" } // start loading just before it scrolls into view
  )

  observer.observe(containerRef.current);

  return () => observer.disconnect()
}, [])
const [mediaVisible, setMediaVisible] = useState(false)
const containerRef = useRef(null)

useEffect(() => {
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        setMediaVisible(true); // load the images / mount the videos
        observer.disconnect(); // only needs to happen once
      }
    },
    { rootMargin: "100px" } // start loading just before it scrolls into view
  )

  observer.observe(containerRef.current);

  return () => observer.disconnect()
}, [])
const [mediaVisible, setMediaVisible] = useState(false)
const containerRef = useRef(null)

useEffect(() => {
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        setMediaVisible(true); // load the images / mount the videos
        observer.disconnect(); // only needs to happen once
      }
    },
    { rootMargin: "100px" } // start loading just before it scrolls into view
  )

  observer.observe(containerRef.current);

  return () => observer.disconnect()
}, [])

Each instance runs its own observer and keeps its own state, so they never interfere with each other. The component near the top loads straight away. The ones further down wait until you reach them. Nothing loads for media you never see.

Clean up external connections. Before any real data passes between a page and another server, a handshake has to happen. You can ask the browser to handle that early so it's ready the moment you need it:

<link rel="preconnect" href="https://example-service.com">
<link rel="dns-prefetch" href="https://example-service.com">
<link rel="preconnect" href="https://example-service.com">
<link rel="dns-prefetch" href="https://example-service.com">
<link rel="preconnect" href="https://example-service.com">
<link rel="dns-prefetch" href="https://example-service.com">

We also stopped a couple of non-essential scripts from blocking the rest of the page by loading them asynchronously:

<script src="widget.js" async></script>
<script src="widget.js" async></script>
<script src="widget.js" async></script>

These are small changes individually. Together they mean the browser spends its first few seconds on things that matter to the visitor.

Why We Replaced a Third-Party 3D Viewer with a Custom Framer Component

One feature relied on a third party's built-in preview to display 3D model files. At first it seemed like the sensible approach — let someone else handle the rendering, write less code ourselves. The catch is that it also handed them the reliability of the feature, and it wouldn't always load as expected. Most of the time it was fine. Every so often it simply wouldn't render, leaving a generic error message telling visitors to download the file instead.

When that happened there wasn't much we could do. The third party controlled the rendering environment — we could only wait.

We decided the convenience wasn't worth it and wrote our own viewer. More work up front, but the whole component could be shaped to our needs: how it loads, what it shows when something breaks, how fast it initializes. The preview now renders every time, with a fallback we control, without an external service's error page appearing in the middle of our client's product showcase.

The broader point: third-party embeds trade control for convenience. That's often worth it. But for anything that directly affects how a product is presented to visitors, it's worth asking whether the convenience is worth the dependency.

Don't Forget to Audit Your Own Code

The last issue was an oversight on our part. A small component we'd built to estimate an article's reading time — a nice touch — was doing far more behind the scenes. It was adding unnecessary hidden elements to the DOM on every render, with no benefit to the output.

We fixed the underlying issue and eventually rebuilt it properly. If you want a reading time indicator for your own Framer site without the performance cost, we sell the optimised version on the Framer Marketplace.

We'd simply missed it. The first version worked, the side effect wasn't obvious from the outside, and it carried on that way until we went looking for things to speed up. Once we found it, the fix took about five minutes: calculate the estimate directly by parsing the article content and drop the hidden markup altogether.

It's a reminder that your own code deserves the same scrutiny as anything you pull from elsewhere. First drafts solve problems. They don't always do it efficiently, and they often carry side effects that only show up when you go looking.

Results: What These Framer Performance Fixes Actually Achieved

None of this was one dramatic fix. It was a cluster of small, targeted improvements:

  • Videos re-encoded from 120fps 1080p to 30fps 720p — the same visual result, a fraction of the data

  • The before-and-after sliders load only the instance visible on screen; everything below the fold waits

  • Third-party 3D render failures dropped to zero after switching to our own component

  • The reading time fix removed dozens of unnecessary hidden DOM elements added on every render

Together they made a real difference — on a brand-new device and on one that's a few years past its prime. The site that used to crash older phones now runs without issue. The one that loaded slowly on a slow connection now loads quickly.

Use formats that work everywhere. Load what's important first and everything else later. Don't make the browser do unnecessary work. And review your own code with the same critical eye you bring to anything external.

If you're working on a Framer site and running into similar performance issues, we help brands with exactly this kind of work → See what we did for Pyrois (Former Helio).

Frequently Asked Questions

What video format should I use in a Framer website?

Use .mp4 with H.264 encoding as your primary format — it has the widest support across all browsers and devices, including older versions of iOS Safari. .webm can be offered as an enhancement for browsers that support it, but .mp4 should always be the fallback. Keep frame rates at 30fps for decorative or background video and match the resolution to the size it's actually displayed at.

How do I lazy load media in Framer?

For standard <img> tags, the loading="lazy" attribute works natively. For media rendered as CSS backgrounds or mounted video elements inside custom Framer components, use the Intersection Observer API to watch each component's position and only load its media when it approaches the viewport. Apply it via a Framer code override or inside the component's useEffect hook.

What causes a Framer website to crash on older phones?

The most common causes are high frame-rate videos (above 30fps), multiple large media files loading simultaneously on page open, and components that trigger too much work on the browser's main thread at once. Encoding videos at 30fps and 720p resolution, and deferring below-fold media with lazy loading, addresses the most frequent sources of crashes.

How do I add fetchpriority to images in Framer?

Use a Framer code override on the image element and add fetchpriority="high" as an attribute. Apply it to your hero or above-the-fold images only — prioritising everything has the same effect as prioritising nothing.

Should I use third-party embeds in Framer?

It depends on what's being embedded. For analytics, consent, and marketing tools, third-party embeds are standard. For features that directly affect how your product is presented to visitors — 3D previews, interactive demos, key UI components — consider whether a custom implementation gives you enough control over reliability and fallback behaviour to justify the extra build time.

How do I speed up a Framer website without changing the design?

Start with videos (format, resolution, frame rate), then media lazy loading for below-fold content, then audit any third-party scripts for unnecessary blocking behaviour. Use async on non-essential scripts, preconnect for external services you depend on, and fetchpriority="high" on your largest above-fold image. None of these changes affect the visual design.

Written by João Ribau, developer at Concealed — a Framer Expert studio based in Lisbon building fast, scalable websites and custom internal tools.

Ready to move faster without compromising quality?

We work with ambitious teams to design and build digital products — fast, in-house, and with measurable results.

João Saraiva, CEO and Founder.

João Saraiva

Founder & CEO

Kick off in as little as 48 hours