Back to Archive
April 30, 2026 18 min read Frontend Engineering

Visual Velocity.

Building High-Performance Landing Pages: Framer Motion, Three.js, and Core Web Vitals

Share Architecture:

A perfectly optimized backend means nothing if your landing page feels janky. Users open your site, scroll through a 3D animation, and the frame rate drops from 60fps to 12fps. They close the tab. Your conversion rate suffers.

This is the performance paradox: the most impressive features—3D graphics, scroll animations, micro-interactions—are also the slowest. Adding a Three.js scene to a landing page can tank your Core Web Vitals instantly.

I learned this building the OrdersPilot landing page. We wanted cinematic 3D scroll animations to demonstrate product flows. It looked incredible, but it also maxed out the CPU and tanked performance on mobile. The solution wasn't removing 3D—it was learning to do it **efficiently**.

The Problem: 3D + Scroll = Jank

The Rendering Bottleneck

  • 1. User scrolls the page
  • 2. Scroll event fires (60+ times per second)
  • 3. Scroll handler updates Three.js camera positions
  • 4. Three.js re-renders the scene (expensive)
  • 5. Framer Motion animates UI elements
  • 6. Browser paints the result
  • 7. Frame takes 50ms to render (should be 16ms) → JANK

The Solution: Scroll Virtualization

The key insight: **don't re-render everything on every scroll event**. Instead, use a scroll virtualization library like **Lenis**. It decouples scroll events from the render loop.

// Smooth Scroll with Lenis

const lenis = new Lenis({
  duration: 1.2,
  smoothWheel: true
});

function raf(time) {
  lenis.raf(time);
  requestAnimationFrame(raf);
}
requestAnimationFrame(raf);

Core Optimizations

01. Three.js with RequestAnimationFrame

Don't sync Three.js to scroll events. Let it render at 60fps independently and update positions based on the current scroll progress.

lenis.on('scroll', (scroll) => {
  scrollProgress = scroll.progress; 
});

function animate() {
  requestAnimationFrame(animate);
  camera.position.x = Math.sin(scrollProgress * Math.PI * 2) * 5;
  renderer.render(scene, camera);
}

02. Lazy Load and Unload 3D Assets

Don't load 10MB scenes until needed. Trigger load when the 3D section comes into view using `IntersectionObserver`.

03. Reduce Geometry Complexity

OrdersPilot's models were simplified from 50,000 polygons to 12,000 using `SimplifyModifier`. Performance improved 60%.

04. Hardware Accelerated Transforms

Animate `transform` (x, y, scale), not layout properties like `left`/`top`. This prevents expensive layout recalculations.

Performance Benchmarks

MetricBeforeAfter
Largest Contentful Paint (LCP)4.2s1.8s
First Input Delay (FID)280ms45ms
Cumulative Layout Shift (CLS)0.180.05
Frame Rate (3D section)22fps59fps

Debugging Performance

When your landing page feels janky: open the Performance tab in Chrome DevTools, record a scroll, and look for red triangles (frames > 16ms). Identify the culprit: layout thrashing, long JS execution, or rendering bottlenecks.

Lessons Learned

01

Scroll Events Are Evil

Decouple scroll input from rendering using libraries like Lenis. Let your renderer run at a fixed 60fps, independent of scroll chaos.

02

Complex Visuals Need Intentional Unloading

3D scenes and heavy animations should only be present when visible. Load on demand, unload when out of view.

03

Transform Everything, Calculate Nothing

The browser is optimized for CSS transforms (cheap, GPU-accelerated). It's not optimized for position changes (expensive, CPU-bound).

High-performance visuals aren't magic. They're discipline: decoupling input from rendering, lazy-loading heavy assets, and respecting the browser's rendering budget.