Visual Velocity.
Building High-Performance Landing Pages: Framer Motion, Three.js, and Core Web Vitals
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
| Metric | Before | After |
|---|---|---|
| Largest Contentful Paint (LCP) | 4.2s | 1.8s |
| First Input Delay (FID) | 280ms | 45ms |
| Cumulative Layout Shift (CLS) | 0.18 | 0.05 |
| Frame Rate (3D section) | 22fps | 59fps |
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
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.
Complex Visuals Need Intentional Unloading
3D scenes and heavy animations should only be present when visible. Load on demand, unload when out of view.
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.