All lessons
FrontendIntermediate11 min readIn my stack

Web animation: GSAP, Lenis & Three.js

Motion that feels smooth, stays fast and respects people who prefer less of it.

  • GSAP
  • ScrollTrigger
  • Lenis
  • Three.js
  • Reduced motion
Step 1/4
At 60 fps the browser has about 16 ms per frame to run JS, recalculate styles, do layout, paint and composite.

In 30 seconds

Good web animation guides attention and makes an interface feel alive; bad animation makes it slow and annoying. GSAP is the standard JavaScript library for timelines and scroll-driven motion, Lenis adds smooth scrolling, and Three.js draws 3D with WebGL. The rules that matter most are simple: animate transform and opacity, clean up after yourself, and always respect prefers-reduced-motion.

Why I use it

  • I use GSAP because its timelines make complex, sequenced motion readable and easy to change, and ScrollTrigger handles scroll-linked effects reliably across browsers.
  • I add Lenis on marketing and brand sites where smooth, consistent scrolling is part of the experience, and wire it to ScrollTrigger so both agree on the scroll position.
  • I reach for Three.js when a brand needs a real 3D moment, such as an interactive scene or a playful mini-game, and keep it lazy-loaded so it never blocks the page.
  • I treat performance and reduced motion as requirements, because an animation that drops frames or makes someone dizzy is a bug, not a feature.

How it works

01How the browser draws a frame

To animate well you need to know what the browser does 60 or more times per second. For each frame it may run JavaScript, recalculate styles, do layout (work out sizes and positions), paint (fill pixels) and composite (stack layers on the GPU). Changing width, height, top or margin forces layout and paint, which is expensive. Changing transform and opacity can usually skip both and happen only in the compositor, which is cheap. That is why smooth animations move things with translate, scale and rotate, and fade with opacity. You have about 16 milliseconds per frame at 60Hz, and less on 120Hz screens, so every unnecessary layout costs you.

  • Cheap: transform (translate, scale, rotate), opacity, filter in many cases.
  • Expensive: width, height, top, left, margin, padding, box-shadow size.
  • Layout thrash: reading layout (offsetHeight) right after writing styles, in a loop.

02GSAP basics and timelines

GSAP animates any numeric property over time. A tween is one animation: gsap.to() animates to values, gsap.from() animates from values to the current state, and gsap.fromTo() sets both ends. A timeline is a sequence of tweens with precise control over overlaps, using the position parameter: '<' starts with the previous tween, '-=0.2' overlaps by 0.2 seconds. Timelines can be paused, reversed, sped up and scrubbed, which makes them perfect for intros and scroll stories. Since 2025 GSAP and all its plugins, including SplitText and ScrollTrigger, are free for commercial use. In React, use the useGSAP hook from @gsap/react: it scopes selectors to a container and reverts every animation on unmount.

tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
 
gsap.registerPlugin(useGSAP);
 
export function Intro() {
  const root = useRef<HTMLDivElement>(null);
  useGSAP(() => {
    const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.8 } });
    tl.from(".title", { y: 40, autoAlpha: 0 })
      .from(".lead", { y: 20, autoAlpha: 0 }, "-=0.5")
      .from(".cta", { scale: 0.9, autoAlpha: 0 }, "<0.2");
  }, { scope: root });
  return <div ref={root}>{/* .title .lead .cta */}</div>;
}

03ScrollTrigger and SplitText

ScrollTrigger connects an animation to the scroll position. You set a trigger element and start and end points such as 'top 80%' (when the element's top reaches 80% down the viewport). With scrub: true the animation follows the scrollbar; with pin: true the element stays fixed while the timeline plays. Use markers: true while developing. SplitText splits text into lines, words or characters so you can stagger them. The 2025 rewrite is smaller, re-splits on resize with autoSplit, can wrap lines in masks for reveal effects, and adds an aria-label to the parent while hiding the split pieces from screen readers. Always wait for fonts to load before splitting, or line breaks will be wrong.

ts
gsap.registerPlugin(ScrollTrigger, SplitText);
 
document.fonts.ready.then(() => {
  SplitText.create(".headline", {
    type: "lines",
    mask: "lines",
    autoSplit: true,
    onSplit: (self) =>
      gsap.from(self.lines, {
        yPercent: 100,
        stagger: 0.08,
        scrollTrigger: { trigger: ".headline", start: "top 80%" },
      }),
  });
});

04Smooth scrolling with Lenis

Lenis is a small library that smooths wheel and trackpad scrolling while keeping native scroll behaviour: the page still scrolls the document, so position: sticky, anchors and accessibility tools keep working. The key integration point is ScrollTrigger: both must use the same clock and the same scroll position. You drive Lenis from GSAP's ticker and tell ScrollTrigger to update on every Lenis scroll event. Turn off GSAP's lag smoothing so they never drift apart. Be careful with nested scroll areas (modals, dropdowns): mark them with data-lenis-prevent so they scroll natively. Respect reduced motion by not starting Lenis at all for those users.

ts
import Lenis from "lenis";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
 
gsap.registerPlugin(ScrollTrigger);
 
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) {
  const lenis = new Lenis({ lerp: 0.1 });
  lenis.on("scroll", ScrollTrigger.update);
  gsap.ticker.add((time) => lenis.raf(time * 1000));
  gsap.ticker.lagSmoothing(0);
}

05Three.js basics

Three.js is a library over WebGL (and now WebGPU) for 3D in the browser. Every scene needs four things: a Scene that holds objects, a Camera that looks at it, a Renderer that draws to a canvas, and Meshes, which are a geometry (the shape) plus a material (how it looks). Lights are needed for most materials. An animation loop updates objects and renders each frame. For performance, cap the pixel ratio at 2, reuse geometries and materials, pause rendering when the canvas is off-screen, and call dispose() on geometries, materials and textures when a component unmounts, otherwise GPU memory leaks. In React projects, React Three Fiber wraps the same ideas in components.

ts
import * as THREE from "three";
 
const canvas = document.querySelector("canvas")!;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
 
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 0.1, 100);
camera.position.z = 4;
 
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1, 1), new THREE.MeshNormalMaterial());
scene.add(mesh);
 
renderer.setAnimationLoop((t) => {
  mesh.rotation.y = t * 0.0004;
  renderer.render(scene, camera);
});

06Performance: transform, will-change and layout thrash

Most janky animations come from three causes. First, animating layout properties; switch to transforms (GSAP's x, y, scale and rotation already use transform). Second, layout thrash: code that writes a style, then reads offsetHeight or getBoundingClientRect, then writes again, forcing the browser to recalculate layout repeatedly inside one frame. Batch all reads first, then all writes. Third, too many layers or a huge painted area. will-change: transform promotes an element to its own GPU layer, which helps for an element that is about to animate, but every layer costs memory, so add it shortly before and remove it after, never on hundreds of elements. Profile in the Chrome DevTools Performance panel: long purple (layout) and green (paint) blocks show the problem.

  • Animate x, y, scale, rotation and autoAlpha in GSAP.
  • Read all measurements first, then write styles.
  • Use will-change sparingly and temporarily.
  • Kill ScrollTriggers and tweens on route change to avoid leaks.

07Reduced motion and accessibility

Some people get dizzy or nauseous from large movement, parallax and zooming. They turn on 'reduce motion' in their operating system, and the browser exposes it as the prefers-reduced-motion media query. Respect it: keep fades and colour changes, but drop big movements, parallax, auto-playing loops and smooth-scroll hijacking. gsap.matchMedia() lets you define animations per condition and reverts them automatically when the condition changes. Equally important: the page must be complete without JavaScript. Content hidden in CSS waiting for an animation to reveal it should have a visible end state if the script fails. Never animate focus away, keep anything that moves for more than five seconds pausable (a WCAG requirement), and avoid flashing more than three times per second.

ts
const mm = gsap.matchMedia();
 
mm.add("(prefers-reduced-motion: no-preference)", () => {
  gsap.from(".card", { y: 60, autoAlpha: 0, stagger: 0.1,
    scrollTrigger: { trigger: ".cards", start: "top 75%" } });
});
 
mm.add("(prefers-reduced-motion: reduce)", () => {
  gsap.from(".card", { autoAlpha: 0, duration: 0.3 });
});

Use it when

  • Brand, agency and campaign sites where motion is part of the story.
  • Sequenced intros, page transitions and scroll-driven storytelling.
  • Micro-interactions that explain state changes, such as adding to cart or opening a panel.
  • Interactive 3D product views or playful scenes when the brand justifies the weight.

Skip it when

  • Simple hover and focus states; CSS transitions are lighter and enough.
  • Data-heavy dashboards and admin tools, where motion slows people down.
  • Smooth scrolling on content-heavy or form-heavy apps where native scrolling is expected.

Alternatives

  • CSS transitions and keyframes

    Zero JavaScript, great for hover states and simple entrances.

  • CSS scroll-driven animations

    animation-timeline: view() and scroll() run off the main thread; browser support is still growing, so keep a fallback.

  • View Transitions API

    Native animated transitions between DOM states and pages, now usable in modern browsers.

  • Motion (formerly Framer Motion)

    Declarative React animation with layout animations; convenient in component-driven apps.

Best practices

  1. Animate transform and opacity; avoid layout properties.
  2. Use timelines with defaults so motion stays consistent and easy to tweak.
  3. Scope and clean up animations with useGSAP or context.revert() on unmount.
  4. Wire Lenis and ScrollTrigger to the same ticker.
  5. Respect prefers-reduced-motion with gsap.matchMedia().
  6. Give every animated element a visible end state without JavaScript.
  7. Lazy-load Three.js scenes and dispose GPU resources on unmount.

Common mistakes

  • Animating top, left or width and wondering why it stutters on phones.
  • Hiding content with opacity: 0 in CSS so it stays invisible if the script fails.
  • Forgetting to kill ScrollTriggers on client-side navigation, leading to wrong positions and leaks.
  • Splitting text before web fonts load, so line breaks shift after the animation starts.
  • Putting will-change on everything, which uses more memory and can make things slower.

Deep dive

08Compositor layers: what the GPU really does

When you animate transform or opacity, the browser can move an element on the compositor thread, separate from the main thread where JavaScript runs. For that the element needs its own layer: a bitmap stored in GPU memory. Browsers promote elements automatically during CSS transitions and Web Animations API transforms, or when you set will-change: transform. Each layer costs roughly width times height times four bytes, multiplied by the device pixel ratio squared, so one full-screen layer on a 3x phone is already more than ten megabytes. Promote a few hundred cards and mobile browsers start dropping frames or even reload the tab. Note the important difference: CSS and Web Animations API animations of transform can keep running even while the main thread is blocked, but JavaScript-driven animations, including GSAP, calculate every frame on the main thread, so long tasks make them stutter. Inspect what is promoted with the DevTools Layers panel and the Rendering tab's layer borders.

  • Layer memory scales with area and pixel ratio squared, not with element count alone.
  • Overlapping a promoted layer can force other elements into layers too (layer explosion).
  • For effects that must survive a busy main thread, prefer CSS or the Web Animations API.

09High-frequency motion: quickTo, overwrite and the ticker

Some motion updates dozens of times per second: a cursor follower, a magnetic button, parallax reacting to the pointer. Creating a new gsap.to() on every pointermove allocates objects and produces tweens that fight each other. gsap.quickTo() builds one reusable function per property that simply retargets a single tween, which is far cheaper. When several tweens may animate the same property, set overwrite: 'auto' so GSAP kills only the overlapping part of the older tweens. All GSAP animation runs on one ticker built on requestAnimationFrame; hook your own per-frame logic into gsap.ticker.add() instead of starting a second loop, so everything stays in sync and pauses together. GSAP also applies lag smoothing after a long frame so motion does not jump. Inside these handlers, never read layout; cache element sizes on resize with a ResizeObserver and only write transforms.

ts
import gsap from "gsap";
 
const xTo = gsap.quickTo(".cursor", "x", { duration: 0.4, ease: "power3" });
const yTo = gsap.quickTo(".cursor", "y", { duration: 0.4, ease: "power3" });
 
window.addEventListener(
  "pointermove",
  (e) => {
    xTo(e.clientX);
    yTo(e.clientY);
  },
  { passive: true },
);
 
// one shared per-frame hook instead of a separate requestAnimationFrame loop
gsap.ticker.add((time, deltaTime) => {
  /* update custom state with deltaTime (ms) */
});

10ScrollTrigger edge cases that break in production

ScrollTrigger calculates start and end positions and caches them, so anything that changes the page height later makes triggers fire in the wrong place: images without dimensions, web fonts swapping in, client-fetched content, accordions opening. Reserve space for media and call ScrollTrigger.refresh() after such changes; it already runs on load and resize. Create triggers in the order they appear on the page, or set refreshPriority, because pinning a section adds spacing that shifts everything below it. On phones, the address bar showing and hiding resizes the viewport; ScrollTrigger.config({ ignoreMobileResize: true }) skips refreshes for those vertical resizes, and heights in svh units stay stable. Pinning inside a transformed parent misbehaves, because position: fixed becomes relative to any ancestor with a transform; pin the outer section instead. In single-page navigation, kill old triggers before new ones are created, or positions from the previous page leak into the next.

ts
gsap.registerPlugin(ScrollTrigger);
 
ScrollTrigger.config({ ignoreMobileResize: true });
 
// recalc once fonts and late content are in place
document.fonts.ready.then(() => ScrollTrigger.refresh());
 
new ResizeObserver(() => ScrollTrigger.refresh()).observe(
  document.querySelector("main")!,
);

11Three.js at scale: draw calls, shaders and WebGPU

A 3D scene is usually limited by draw calls and shader work, not by triangle count. Each mesh with its own material is at least one draw call, so a thousand identical objects as separate meshes cost a thousand calls; one InstancedMesh draws them all in a single call with a matrix per instance. Merge static geometry and share materials so the GPU switches state less often. The first frame that uses a new material compiles its shader on the main thread, which can freeze the page for hundreds of milliseconds; precompile with renderer.compileAsync() before revealing the scene. For scenes that change only on interaction, render on demand instead of every frame (in React Three Fiber, frameloop='demand'). Watch renderer.info for live draw calls, triangles, geometries and textures; growing numbers across navigations mean a leak. Three.js also ships a WebGPURenderer that falls back to WebGL 2 and enables compute shaders, but plain WebGL remains the safest choice when broad device support matters.

ts
import * as THREE from "three";
 
const count = 2000;
const mesh = new THREE.InstancedMesh(
  new THREE.SphereGeometry(0.05, 12, 12),
  new THREE.MeshStandardMaterial({ color: 0xdddddd }),
  count,
);
const m = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
  m.setPosition(Math.random() * 10 - 5, Math.random() * 10 - 5, Math.random() * -10);
  mesh.setMatrixAt(i, m);
}
scene.add(mesh); // 2,000 spheres, one draw call
 
await renderer.compileAsync(scene, camera); // avoid first-frame shader jank
console.log(renderer.info.render.calls);

Real-world scenarios

01

A pinned section jumps and misaligns on iPhones

Problem

A pinned storytelling section works on desktop, but on iPhones it jumps when the address bar collapses, and sometimes unpins too early after images finish loading.

How to solve it

Reproduce first on a real device with Safari's remote Web Inspector and markers: true. Then fix the causes one by one. Give every image width and height, or aspect-ratio, so late loads do not change the page height. Call ScrollTrigger.refresh() after document.fonts.ready and after client-fetched content renders. Set ScrollTrigger.config({ ignoreMobileResize: true }) and size full-height panels with 100svh instead of 100vh. Check that no ancestor of the pinned element has a transform, filter or will-change, which breaks fixed positioning. Finally, under prefers-reduced-motion skip the pin entirely through gsap.matchMedia() and show the steps as a normal vertical list.

02

INP got worse after a new animated hero launched

Problem

Field INP rose from about 150 ms to over 300 ms on mid-range Android phones after a new hero with split text, many scroll effects and a small 3D scene. The animation itself looks smooth.

How to solve it

Record a trace in DevTools with 4x CPU throttling and click around during load; look for long tasks overlapping interactions. Usual culprits: SplitText splitting hundreds of characters in one task, dozens of ScrollTriggers created at once, shader compilation on first render, pointermove handlers reading layout. Split by words or lines instead of characters, create below-the-fold effects only when an IntersectionObserver says they are near, and yield between setup chunks with scheduler.yield() where supported. Precompile the 3D scene with compileAsync and lazy-load Three.js after the page is interactive. Confirm in the field with the web-vitals attribution build, which names the slow interaction and script.

03

Memory grows on every client-side navigation

Problem

On a Next.js portfolio, after visiting ten case studies the tab uses over a gigabyte and scrolling stutters. Each page has a Three.js canvas and scroll animations.

How to solve it

Take heap snapshots in the DevTools Memory panel before and after a few navigations and compare detached DOM nodes and retained WebGL objects. On unmount, revert GSAP through useGSAP, call lenis.destroy() if each page creates Lenis, stop the loop with renderer.setAnimationLoop(null), traverse the scene to dispose geometries, materials and textures, then call renderer.dispose(). Disconnect ResizeObservers and remove window listeners. Browsers limit the number of live WebGL contexts and drop the oldest with a warning, so a better architecture is one persistent canvas in the root layout that swaps scenes between routes instead of creating a new renderer per page.

Cheat sheet

Retarget one property cheaply
const xTo = gsap.quickTo(el, "x", { duration: 0.3 }); xTo(120);
Stop overlapping tweens fighting
gsap.to(el, { x: 100, overwrite: "auto" });
Recalculate trigger positions
ScrollTrigger.refresh();
Ignore address-bar resizes on mobile
ScrollTrigger.config({ ignoreMobileResize: true });
Slow every animation down to debug
gsap.globalTimeline.timeScale(0.2);
Kill all triggers on route change
ScrollTrigger.getAll().forEach((t) => t.kill());
Stop a Three.js render loop
renderer.setAnimationLoop(null);
Precompile shaders before showing a scene
await renderer.compileAsync(scene, camera);
Count draw calls per frame
renderer.info.render.calls
Render only when something changes (R3F)
<Canvas frameloop="demand">

In my real projects

  • I use GSAP on several Next.js projects, including a restaurant-chain ordering site, a pet-supplies storefront, a winery shop and a digital agency website.
  • On the agency website rewrite, the frontend combined GSAP with a three.js mini-game, a particle cursor and Lottie animations, alongside a dark mode.
  • A winery direct-to-consumer site I worked on includes a 3D scene built with Three.js.
  • This site uses GSAP with ScrollTrigger, SplitText and Flip plus Lenis, driven by data attributes so server components never import GSAP, with reduced motion respected and a visible end state for every effect.

Questions & answers

Q1Why should you animate transform and opacity instead of top or width?

Because of the rendering pipeline. Changing top, left, width or margin changes geometry, so the browser must recalculate layout, repaint pixels and then composite, often for many elements, on every frame. Transform and opacity can usually be handled by the compositor on the GPU without layout or paint, so they stay smooth even on mid-range phones. In practice I move elements with translate and scale, fade with opacity, and if I really need a size change I animate scale and correct the content, or use a technique like FLIP, which measures the start and end layouts once and animates the difference as a transform.

Q2What is layout thrashing and how do you avoid it?

Layout thrashing happens when code alternates between writing styles and reading layout values inside the same frame. For example, a loop that sets an element's height and then reads offsetHeight of the next one. Each read forces the browser to recalculate layout synchronously so it can return a correct number. With many elements this becomes dozens of forced layouts per frame. The fix is to batch: read all measurements first, store them, then apply all writes. Animation libraries like GSAP already batch internally, and requestAnimationFrame helps schedule writes. The DevTools Performance panel flags forced reflows, which makes it easy to find.

Q3How do you use GSAP correctly in React and Next.js?

GSAP touches the DOM, so it runs only in client components. I use the useGSAP hook from @gsap/react: it runs after the DOM is ready, scopes selector strings to a container ref so .title only matches inside that component, and automatically reverts every tween and ScrollTrigger when the component unmounts. That matters in Next.js, where client-side navigation would otherwise leave stale triggers. For event-driven animations created later, I wrap them with contextSafe. I also keep GSAP out of server components entirely, for example by marking elements with data attributes and letting one client-side provider attach effects.

Q4How do you respect prefers-reduced-motion?

I treat it as a separate motion design, not an off switch. With gsap.matchMedia() I register the full animations under no-preference and a simpler set under reduce: short fades and colour changes instead of movement, no parallax, no pinned scroll scenes, no smooth scrolling. matchMedia also reverts animations automatically if the user changes the setting while the page is open. In CSS I wrap transitions that move things in a no-preference media query. And regardless of motion settings, the content must be fully visible and usable if JavaScript is slow or disabled.

Q5Does smooth scrolling hurt accessibility or performance?

It can, if done badly. Old libraries replaced native scrolling with a transformed container, which broke position: sticky, anchors, find-in-page and assistive tools. Lenis keeps native document scrolling and only smooths the input, so those keep working. Still, I disable it for prefers-reduced-motion, let nested scroll areas scroll natively with data-lenis-prevent, and keep keyboard scrolling intact. For performance, Lenis itself is light; the real cost is whatever runs on every scroll, so scroll-linked effects should only animate transforms and use ScrollTrigger rather than custom scroll listeners. On app-like or form-heavy pages, I simply leave it out.

Q6How do you keep a Three.js scene from hurting page performance?

First, load it lazily: import Three.js dynamically only when the canvas is near the viewport, so it never delays the main content or LCP. Cap the device pixel ratio at 2, keep polygon counts and texture sizes reasonable, and use compressed formats such as KTX2 textures and Draco or Meshopt geometry. Pause the render loop when the canvas is off-screen or the tab is hidden. Share geometries and materials between meshes. On unmount, dispose geometries, materials, textures and the renderer to free GPU memory. Finally, offer a static image fallback for reduced motion and low-power devices.

Senior-level questions

S1GSAP, CSS animations or the Web Animations API: how do you decide per effect?

I decide by what the effect needs. Simple state changes such as hover, focus and open or close panels use CSS transitions: zero JavaScript, and transform and opacity animations can run on the compositor even when the main thread is busy. Entrance effects tied to scroll position can use CSS scroll-driven animations where support allows, with a static fallback. I use the Web Animations API when I need JavaScript control, such as reversing or awaiting finish, without a library. GSAP earns its weight for sequenced timelines, scrubbed and pinned scroll scenes, text splitting, SVG morphing and cross-browser consistency. Its cost is main-thread work per frame and bundle size, so I load plugins only where used. A typical site mixes all three, and the rule is the lightest tool that does the job reliably.

S2How do you architect animation in a large Next.js site so server components stay animation-free?

I separate description from execution. Server components only render markup with data attributes, such as data-anim='fade-up' or data-anim-pin, plus their final visible state, so the page is complete without JavaScript. One client provider mounted in the layout scans for those attributes after hydration, maps each name to a factory function in a small registry, and creates the animations inside a GSAP context scoped to the page. On route change it reverts that context, which kills tweens and triggers in one call, then scans the new page. Heavy pieces, such as Three.js or SplitText, are dynamically imported only when an element needing them exists. gsap.matchMedia() wraps everything for reduced motion and breakpoints. The trade-off is a little indirection, but bundles stay small, server components stay pure and every effect is cleaned up consistently.

S3How do you set and enforce a performance budget for motion-heavy pages?

I set budgets in user terms: smooth frames on a mid-range Android phone, no long task over 50 ms caused by animation setup, LCP and INP unchanged from the page without motion, and a JavaScript budget for animation code, for example GSAP core plus only the plugins in use. Enforcement has three layers. In development I profile with CPU throttling and check frames and long tasks in the Performance panel. In CI, Lighthouse on key pages tracks Total Blocking Time and bundle-size limits fail the build when animation code grows. In production, the web-vitals library reports INP with attribution, so a regression points at the script responsible. When a budget is exceeded, I simplify the effect or move it to CSS rather than accept the cost, because motion should never make the page slower to use.

Glossary

Compositor thread
A browser thread that combines layers into the final image; it can move layers without waiting for JavaScript.
Layer promotion
Giving an element its own GPU bitmap so it can be moved or faded cheaply, at a memory cost.
Main thread
The single thread that runs JavaScript, style, layout and paint; long tasks here cause jank.
quickTo
A GSAP helper that returns a reusable function to retarget one property's tween, ideal for pointer-driven motion.
Lag smoothing
GSAP's adjustment after a long frame so animations continue smoothly instead of jumping ahead.
Draw call
One command from the CPU telling the GPU to draw a batch of geometry with one material.
InstancedMesh
A Three.js object that draws many copies of one geometry and material in a single draw call.
Shader compilation
Turning a material's GPU program into machine code; happens on first use and can freeze a frame.
WebGPU
The modern browser graphics and compute API that succeeds WebGL, with lower overhead and compute shaders.

Have a product in mind?Let's build it.

Tell me about your idea, your product or your team. I usually reply within one working day.