This document outlines advanced optimization strategies that have NOT been implemented yet. These are asset-level and scheduling optimizations that can achieve 2-3 second time-to-interactive.
Status: Implementation Pending
Priority: High
Expected Impact: 60-70% improvement in load time and scroll performance
Current State: Single avyaan_coloured.glb with assembly animation
Required Change: Create two separate assets
- Open
avyaan_coloured.glbin Blender - Save As →
hero_static.blend - Apply All Modifiers:
- Select all objects → Object → Apply → All Modifiers
- Join Meshes:
- Select similar objects → Ctrl+J (Join)
- Goal: Reduce to 1-3 mesh objects
- Remove Armatures/Rigs:
- Delete any skeleton/bone structures
- Delete Animation Tracks:
- Go to Dope Sheet → Delete all keyframes
- Go to NLA Editor → Delete all strips
- Export → GLTF 2.0
- Filename:
hero_static.glb - ✅ Include: Meshes, Materials
- ❌ Exclude: Animations, Armatures, Cameras, Lights
- Filename:
- File size: < 500KB (vs current ~2-3MB)
- Draw calls: 1-2 (vs current 15-20)
- Load time: < 300ms
hero_assembly.glb(current file, renamed)- Will be lazy-loaded after interactivity
Tool Required: Install globally
npm install -g @gltf-transform/cli# Hero model
gltf-transform draco public/models/avyaan_coloured.glb public/models/avyaan_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
# Rover models
gltf-transform draco public/models/prayan.glb public/models/prayan_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
gltf-transform draco public/models/abhyan.glb public/models/abhyan_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
gltf-transform draco public/models/vidyaanAR-v3.glb public/models/vidyaanAR-v3_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
# Drone models
gltf-transform draco public/models/akshayaan_compressed.glb public/models/akshayaan_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
gltf-transform draco public/models/nabhyaan.glb public/models/nabhyaan_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12
gltf-transform draco public/models/jatayu_compressed.glb public/models/jatayu_draco.glb \
--quantize-position 14 \
--quantize-normal 10 \
--quantize-texcoord 12- 60-80% size reduction
- Faster GPU parsing
- Lower memory consumption
Update file paths in code to use _draco.glb versions
Why: GPU-native format, eliminates transcoding stutter
# Convert to KTX2 with ETC1S compression (smaller, good for web)
gltf-transform etc1s public/models/avyaan_draco.glb public/models/avyaan_final.glb
# Repeat for all other models
gltf-transform etc1s public/models/prayan_draco.glb public/models/prayan_final.glb
# ... and so on- Never exceed 2K textures (2048x2048)
- Prefer 1K (1024x1024) for non-hero models
- Reduce texture resolution in Blender if needed
- Massive VRAM reduction
- Eliminates scroll spikes caused by texture uploads
- Faster GPU processing
Why: CAD-level geometry is overkill for web
# Reduce geometry by 50% (visually identical in motion)
gltf-transform simplify public/models/prayan_draco.glb public/models/prayan_simplified.glb \
--ratio 0.5
# Apply to all secondary models (NOT hero initially)- Open model in Blender
- Select mesh → Modifiers → Add Decimate
- Set Ratio to 0.5 (50% reduction)
- Apply modifier
- Re-export
- Start with 50% reduction
- Test visually
- Can go up to 70% for distant/small models
- ≤ 4 materials per model
- Merge materials with same colors
- Bake textures if needed
- Open model in Blender
- Material View → Check material count
- Identify duplicates (same color/properties)
- Select objects with duplicate materials
- Assign single material to all
- Delete unused materials from slots
- Re-export
- Fewer draw calls
- Lower GPU state changes
- Faster rendering
Current: Hero loads and animates immediately, blocking interactivity
New Timeline:
0.0s → Page loads
0.5s → Navbar interactive ✅
1.0s → Static hero visible ✅
2.0s → Assembly animation loads (lazy) ✅
2.5s → Animation starts ✅
File: sections/hero-section.tsx
const [showAnimatedHero, setShowAnimatedHero] = useState(false)
// Load static hero immediately
const StaticHero = dynamic(() => import('./HeroStatic'), { ssr: false })
// Load animated hero lazily
const AnimatedHero = dynamic(() => import('./HeroAnimated'), { ssr: false })
useEffect(() => {
// Use requestIdleCallback to defer animation loading
const id = requestIdleCallback(() => {
setShowAnimatedHero(true)
}, { timeout: 2000 })
return () => cancelIdleCallback(id)
}, [])
return (
<>
{!showAnimatedHero && <StaticHero />}
{showAnimatedHero && <AnimatedHero />}
</>
)sections/HeroStatic.tsx- Useshero_static.glbsections/HeroAnimated.tsx- Useshero_assembly.glb
Current: All models preload upfront with Promise.all()
Problem: Blocks main thread
Solution: Progressive idle-time warm-up
File: lib/useWarmModels.ts
import { useEffect } from 'react'
import { useGLTF } from '@react-three/drei'
export function useWarmModels(models: string[]) {
useEffect(() => {
let i = 0
function warmNext() {
if (i >= models.length) return
// Preload one model at a time during idle
useGLTF.preload(models[i++])
// Schedule next warm-up during idle time
requestIdleCallback(warmNext, { timeout: 500 })
}
// Start warming after initial render
requestIdleCallback(warmNext, { timeout: 1000 })
}, [models])
}// Replace current preload3DModels() with:
useWarmModels([
"/models/prayan_final.glb",
"/models/abhyan_final.glb",
"/models/vidyaanAR-v3_final.glb",
"/models/akshayaan_final.glb",
"/models/nabhyaan_final.glb",
"/models/jatayu_final.glb",
])Benefits:
- Non-blocking
- Uses idle CPU time
- Doesn't delay interactivity
Problem: Models loaded in memory but not uploaded to GPU = scroll hitch
Solution: Offscreen Canvas that pre-uploads to GPU
File: components/GPUWarmup.tsx
'use client'
import { Canvas } from '@react-three/fiber'
import { useGLTF } from '@react-three/drei'
function WarmModel({ url }: { url: string }) {
const { scene } = useGLTF(url)
return <primitive object={scene} visible={false} />
}
export function GPUWarmup({ models }: { models: string[] }) {
return (
<Canvas
style={{
position: 'fixed',
top: 0,
left: 0,
width: 1,
height: 1,
opacity: 0,
pointerEvents: 'none',
zIndex: -1
}}
frameloop="demand"
>
{models.map(url => (
<WarmModel key={url} url={url} />
))}
</Canvas>
)
}{allAssetsReady && (
<GPUWarmup models={[
"/models/prayan_final.glb",
"/models/abhyan_final.glb",
// ... all models
]} />
)}Result: Zero scroll hitching when reaching 3D sections
Add to warm-up system
File: components/GPUWarmup.tsx
import { useThree } from '@react-three/fiber'
import { useEffect } from 'react'
function WarmModel({ url }: { url: string }) {
const { scene } = useGLTF(url)
const { gl, camera } = useThree()
useEffect(() => {
if (scene) {
// Force shader compilation
gl.compile(scene, camera)
}
}, [scene, gl, camera])
return <primitive object={scene} visible={false} />
}Benefit: Eliminates first-frame shader compile stutter
Current: Canvas always rendering (even when not visible)
Solution: Render on-demand with IntersectionObserver
File: components/three/RoverCanvas.tsx
import { Canvas, useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
export function RoverCanvas({ onLoaded }) {
const containerRef = useRef<HTMLDivElement>(null)
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ threshold: 0.1 }
)
if (containerRef.current) {
observer.observe(containerRef.current)
}
return () => observer.disconnect()
}, [])
return (
<div ref={containerRef}>
<Canvas
frameloop={isVisible ? "always" : "demand"}
// ... other props
>
<RoverScene onLoaded={onLoaded} />
</Canvas>
</div>
)
}Apply to all Canvas instances in:
sections/our-rover.tsxsections/our-drone.tsx
Benefit: Saves 50-70% GPU/CPU when scrolled away
Problem: Multiple <Canvas> = Multiple WebGL contexts = High memory
Solution: Single shared Canvas with content swapping
- Keep ONE Canvas mounted
- Swap scene content based on scroll position
- Use portals for positioning
File: components/three/UnifiedCanvas.tsx
export function UnifiedCanvas() {
const [activeScene, setActiveScene] = useState('hero')
return (
<Canvas frameloop="demand">
{activeScene === 'hero' && <HeroScene />}
{activeScene === 'rover1' && <RoverScene1 />}
{activeScene === 'rover2' && <RoverScene2 />}
{/* ... etc */}
</Canvas>
)
}Note: This is a major refactor. Only do if other optimizations insufficient.
Why: Antialiasing is expensive, only needed for hero close-up
File: sections/our-rover.tsx, sections/our-drone.tsx
<Canvas
gl={{
antialias: false, // Disable for non-hero sections
powerPreference: 'high-performance',
// ... other settings
}}
/>Keep antialiasing only in hero:
components/three/RoverCanvas.tsx(hero) -antialias: true- All other Canvas instances -
antialias: false
Benefit: 15-25% performance gain in secondary 3D sections
Problem: Real assembly animation is heavy (per-frame calculations)
Solution: Fake it with simple transforms
- Per-frame mesh traversal
- Material cloning in loops
- Skeleton recalculations
File: components/three/Rover.tsx
// Instead of complex assembly animation:
const parts = useMemo(() => [
{ name: 'body', startPos: [0, -2, 0], endPos: [0, 0, 0], delay: 0 },
{ name: 'wheel1', startPos: [-3, 0, 0], endPos: [-1, 0, 0], delay: 0.2 },
{ name: 'wheel2', startPos: [3, 0, 0], endPos: [1, 0, 0], delay: 0.2 },
// ... etc
], [])
// Animate with simple position lerp
useFrame((state, delta) => {
parts.forEach(part => {
const mesh = scene.getObjectByName(part.name)
if (mesh && progress > part.delay) {
mesh.position.lerp(part.endPos, 0.1)
}
})
})Benefits:
- 50-70% less CPU usage during animation
- Smoother animation
- No frame drops
- Split hero into
hero_static.glb+hero_assembly.glb - Run Draco compression on all 7 models
- Run texture optimization (KTX2)
- Run mesh simplification (50% reduction)
- Optimize materials in Blender (≤4 per model)
- Create
lib/useWarmModels.tshook - Replace bulk preload with idle warm-up
- Create
components/GPUWarmup.tsx - Add GPU warm-up to page
- Split hero into Static + Animated components
- Add shader compilation to warm-up
- Add frameloop="demand" with IntersectionObserver
- Disable antialiasing in non-hero sections
- Simplify assembly animation (fake transforms)
- Test load time (target: 2-3s)
- Test scroll performance (target: 60fps)
- Test memory usage (target: <600MB)
- Test on mobile devices
| Metric | Current | After Phase 1 | After Phase 2-3 |
|---|---|---|---|
| TTI | 5-8s | 3-4s | 2-3s |
| First Paint | 2-3s | 0.8s | 0.5s |
| Scroll Hitch | 100-300ms | 50-100ms | 0-50ms |
| Memory Usage | 700MB | 500MB | 400MB |
| Mobile Usability | Poor | Fair | Good |
- Asset re-authoring is mandatory - Code optimizations alone won't achieve 2-3s load
- Test after each phase - Don't skip validation
- Backup original files - Keep non-optimized versions
- Mobile test early - Desktop performance doesn't predict mobile
- Use Chrome DevTools Performance tab - Profile before/after each change
Must Do (80% of gains):
- Split hero model
- Draco compression
- Idle warm-up hook
- GPU warm-up component
Should Do (15% of gains): 5. Texture optimization 6. Mesh simplification 7. frameloop="demand"
Nice to Have (5% of gains): 8. Disable antialiasing 9. Fake assembly animation 10. One-canvas architecture
Next Steps: Start with Phase 1 (Asset Preparation) using Blender and gltf-transform CLI tools.