Navigate
Search topics across all sections
Go Live
Your R3F app works beautifully on localhost. Now you need to ship it to real users on real devices over real networks. Deployment is not just "push to Vercel" -- it is the difference between a 2-second load and a 12-second load.
You deploy your R3F app to Vercel. It works on your laptop. Then your client opens it on their phone over 4G. The page takes 15 seconds to load, the 3D scene stutters at 8fps, and the browser crashes after 30 seconds. Your Lighthouse score is 23.
Deploy: Bundle size 2.4MB (uncompressed) | LCP: 8.2s | FPS on mobile: 8 | Memory: 450MB (phone limit: 512MB) | Lighthouse: 23/100
Real-world
Think of deployment like launching a rocket.
Pre-flight checks (build optimization) ensure nothing is broken. Fuel efficiency (bundle size) determines how far you can go. The launch pad (hosting platform) must match your rocket. And mission control (monitoring) tells you if something goes wrong in orbit.
One bad setting -- an uncompressed texture, an unoptimized import, a missing environment variable -- can abort the entire mission.
Build
next build
Analyze
Bundle size check
Optimize
Tree-shake, split
Deploy
Vercel / Netlify
Monitor
Lighthouse, errors
Step 1: Build & Bundle Optimization
The build step transforms your development code into optimized production assets. Every kilobyte matters -- especially for 3D apps where Three.js alone is substantial.
Tree-shake Three.js imports
// Before: imports entire Three.js (~600KB)
import * as THREE from 'three'
// After: imports only what you use (~50KB typical)
import { Vector3, Color, MathUtils } from 'three'
import { ACESFilmicToneMapping, SRGBColorSpace } from 'three'
// R3F and drei already tree-shake internally,
// but YOUR code must use named imports tooNamed imports let the bundler eliminate unused code. A typical R3F app only uses 10-15% of Three.js classes. Tree-shaking can save 200-400KB from your final bundle.
Dynamic imports for code splitting
import dynamic from 'next/dynamic'
// Three.js code is split into a separate chunk
const Scene3D = dynamic(
() => import('@/features/scene/Scene3D'),
{
ssr: false, // Required — Three.js needs WebGL
loading: () => (
<div className="h-screen bg-black animate-pulse" />
),
}
)
// Users who never scroll to the 3D section
// never download Three.js at allDynamic imports create separate JavaScript chunks. For an R3F landing page where the 3D scene is below the fold, this means the critical path only includes your HTML and CSS. The 3D code loads on demand.
Analyze your bundle
# Install the analyzer
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)
# Run the analysis
ANALYZE=true npm run build
# Look for:
# - three.js: should be <200KB gzipped
# - Duplicate dependencies
# - Unused packagesBundle analysis shows you exactly what is in your production build. Common findings: duplicate three.js copies (from different versions), unused drei helpers, and development-only packages that snuck into production.
Step 2: Asset Optimization
3D assets (models, textures, HDR maps) are often larger than your entire JavaScript bundle. Compressing them is the highest impact optimization you can make.
Compress models and textures
# Compress GLTF models with Draco (70-90% smaller)
npx gltf-pipeline -i model.glb -o model-draco.glb -d
# Compress textures to KTX2/Basis (4-6x smaller)
npx toktx --t2 --bcmp output.ktx2 input.png
# In your R3F code, use the Draco decoder
import { useGLTF } from '@react-three/drei'
// drei auto-detects Draco and loads the decoder
# Asset size targets:
# - GLTF models: < 1MB after Draco
# - Textures: < 500KB each (KTX2)
# - HDR maps: < 2MB (use compressed .hdr or .exr)
# - Total scene: < 5MB for fast mobile loadingDraco compression reduces GLTF geometry by 70-90%. KTX2/Basis textures are GPU-compressed, meaning they stay compressed even in video memory. Combine both for scenes that load fast and use less GPU RAM.
Serve assets from a CDN
// Store heavy assets on a CDN, not in your repo
// Vercel, Cloudflare R2, or AWS S3 + CloudFront
const MODEL_URL = process.env.NEXT_PUBLIC_CDN_URL
+ '/models/car-draco.glb'
function CarModel() {
const { scene } = useGLTF(MODEL_URL)
return <primitive object={scene} />
}
// next.config.js — set proper cache headers
const nextConfig = {
async headers() {
return [{
source: '/assets/:path*',
headers: [
{ key: 'Cache-Control',
value: 'public, max-age=31536000, immutable' }
],
}]
},
}CDNs serve assets from the edge node closest to the user. A 2MB model that takes 4 seconds from your origin server might take 400ms from a CDN. Set immutable cache headers so returning visitors never re-download unchanged assets.
Step 3: Deployment
Vercel and Netlify are the most common hosts for Next.js + R3F apps. Both handle builds, CDN distribution, and HTTPS automatically.
Vercel deployment checklist
# 1. Environment variables
# Set in Vercel dashboard, NOT in .env files
NEXT_PUBLIC_CDN_URL=https://assets.example.com
# 2. Build command (automatic for Next.js)
next build
# 3. Common gotchas:
# - ssr: false on all dynamic Three.js imports
# - process.env.NODE_ENV auto-set to 'production'
# - Remove r3f-perf and Leva in production:
{process.env.NODE_ENV === 'development' && <Perf />}
# 4. Verify output
# - Check Functions tab for serverless function size
# - Check Edge Network for static asset caching
# - Run Lighthouse on the deployed URLVercel auto-detects Next.js and handles the build. The main things to verify: environment variables are set, Three.js components use ssr: false, and development tools like r3f-perf are conditionally excluded from production bundles.
Production environment checks
// Remove debug tools in production
const isDev = process.env.NODE_ENV === 'development'
function Scene() {
return (
<Canvas>
{isDev && <Perf position="top-left" />}
{isDev && <axesHelper args={[5]} />}
{isDev && <gridHelper args={[10, 10]} />}
<MyScene />
</Canvas>
)
}
// Leva panels — hide in production
<Leva hidden={!isDev} />Development tools add overhead: r3f-perf polls performance stats every frame, Leva creates DOM elements, and helpers add draw calls. Gate them behind NODE_ENV checks so they are tree-shaken out of the production build entirely.
What you just learned
Named imports from 'three' enable tree-shaking — saving 200-400KB vs import * as THREE.
Dynamic imports with ssr: false code-split Three.js so it only loads when needed.
Draco-compressed GLTF models are 70-90% smaller. KTX2 textures are 4-6x smaller.
CDN hosting with immutable cache headers eliminates re-downloads for returning visitors.
Gate development tools (r3f-perf, Leva, helpers) behind NODE_ENV so they are removed from production builds.
Question
Your R3F app has a Lighthouse performance score of 45. The main issues are: LCP of 6.2 seconds and Total Blocking Time of 1.8 seconds. The 3D scene is below the fold. What is the single highest-impact fix?
Think about it...
You deploy an R3F app. Bundle analysis shows three.js appearing twice in your build — once at 580KB and once at 320KB. What is the most likely cause, and how do you fix it?
Hint: What happens when two npm packages list different versions of the same dependency?
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
import * as THREE from 'three'
// Only using Vector3 and Color, but shipping
// the entire Three.js library to every user
const pos = new THREE.Vector3()
const col = new THREE.Color('#ff0000')// page.tsx — this loads immediately even if user
// never scrolls to the 3D section
import { HeavyScene } from './HeavyScene'
export default function Page() {
return (
<div>
<Hero />
<Features />
<HeavyScene /> {/* 500KB component */}
</div>
)
}// No device testing — assuming desktop performance
<Canvas>
<Model /> {/* 50MB uncompressed GLTF */}
<Shadows />
<PostProcessing />
<Particles count={100000} />
</Canvas>Best Practices
Run Lighthouse Before Shipping
Run Lighthouse on your deployed URL, not localhost. Aim for Performance above 70, LCP under 2.5 seconds, and TBT under 200ms. R3F apps rarely hit 100 due to WebGL overhead, but 70+ is achievable with proper optimization.
Test on Real Devices
Chrome DevTools device simulation does not emulate GPU limitations. Test on an actual mid-range Android phone (Samsung A-series or Pixel 6a). If it runs smoothly there, it will run smoothly everywhere.
Set Up Error Monitoring
WebGL context loss, shader compilation failures, and out-of-memory crashes happen in production. Use Sentry or a similar service with a custom error boundary around your Canvas to catch and report these issues.
Add a WebGL Fallback
Not all browsers support WebGL2. Wrap your Canvas in an error boundary that shows a static image or message when WebGL is unavailable. This prevents blank pages on older devices and in corporate environments with GPU restrictions.