Վեբ անիմացիա. GSAP, Lenis և Three.js
Շարժում, որը սահուն է, արագ է մնում և հարգում է նրանց, ովքեր ավելի քիչ շարժում են նախընտրում։
30 վայրկյանում
Լավ վեբ անիմացիան ուղղորդում է ուշադրությունը և ինտերֆեյսը կենդանի է դարձնում, վատ անիմացիան այն դարձնում է դանդաղ և նյարդայնացնող։ GSAP-ը timeline-ների և scroll-ով կառավարվող շարժման ստանդարտ JavaScript գրադարանն է, Lenis-ը ավելացնում է սահուն scroll, իսկ Three.js-ը WebGL-ով 3D է նկարում։ Ամենակարևոր կանոնները պարզ են. անիմացիա արեք transform և opacity, մաքրեք ձեր հետքերը և միշտ հարգեք prefers-reduced-motion-ը։
Ինչու եմ օգտագործում
- GSAP օգտագործում եմ, որովհետև նրա timeline-ները բարդ, հաջորդական շարժումը դարձնում են ընթեռնելի և հեշտ փոփոխվող, իսկ ScrollTrigger-ը scroll-ին կապված էֆեկտները հուսալիորեն է աշխատեցնում բոլոր բրաուզերներում։
- Lenis ավելացնում եմ մարքեթինգային և բրենդային կայքերում, որտեղ սահուն, հետևողական scroll-ը փորձառության մաս է, և այն կապում եմ ScrollTrigger-ի հետ, որպեսզի երկուսն էլ համաձայն լինեն scroll-ի դիրքի հարցում։
- Three.js-ին դիմում եմ, երբ բրենդին իրական 3D պահ է պետք, օրինակ՝ ինտերակտիվ տեսարան կամ խաղային մինի-խաղ, և այն lazy-load եմ անում, որպեսզի երբեք չարգելափակի էջը։
- Արտադրողականությունը և reduced motion-ը համարում եմ պարտադիր պահանջներ, որովհետև frame-եր կորցնող կամ ինչ-որ մեկի գլուխը պտտեցնող անիմացիան բագ է, ոչ թե ֆիչա։
Ինչպես է աշխատում
01Ինչպես է բրաուզերը նկարում frame-ը
Լավ անիմացիա անելու համար պետք է իմանալ, թե ինչ է անում բրաուզերը վայրկյանում 60 կամ ավելի անգամ։ Յուրաքանչյուր frame-ի համար այն կարող է գործարկել JavaScript, վերահաշվել style-երը, անել layout (հաշվել չափերն ու դիրքերը), paint (լցնել պիքսելները) և composite (շերտերը դասավորել GPU-ի վրա)։ width-ի, height-ի, top-ի կամ margin-ի փոփոխությունը ստիպում է layout և paint անել, ինչը թանկ է։ transform-ի և opacity-ի փոփոխությունը սովորաբար կարող է շրջանցել երկուսն էլ և տեղի ունենալ միայն compositor-ում, ինչը էժան է։ Ահա թե ինչու սահուն անիմացիաները տարրերը տեղաշարժում են translate-ով, scale-ով և rotate-ով, իսկ fade-ն անում են opacity-ով։ 60Hz-ի դեպքում մեկ frame-ի համար ունեք մոտ 16 միլիվայրկյան, իսկ 120Hz էկրաններում՝ ավելի քիչ, ուստի յուրաքանչյուր ավելորդ layout ձեզ թանկ է նստում։
- Էժան՝ transform (translate, scale, rotate), opacity, շատ դեպքերում՝ filter։
- Թանկ՝ width, height, top, left, margin, padding, box-shadow-ի չափը։
- Layout thrash. ցիկլում style-եր գրելուց անմիջապես հետո layout կարդալ (offsetHeight)։
02GSAP-ի հիմունքներ և timeline-ներ
GSAP-ը ժամանակի ընթացքում անիմացիա է անում ցանկացած թվային հատկություն։ Tween-ը մեկ անիմացիա է. gsap.to()-ն անիմացիա է անում դեպի արժեքները, gsap.from()-ը՝ արժեքներից դեպի ընթացիկ վիճակը, իսկ gsap.fromTo()-ն սահմանում է երկու ծայրերն էլ։ Timeline-ը tween-ների հաջորդականություն է՝ համընկնումների ճշգրիտ կառավարմամբ position պարամետրի միջոցով. '<'-ը սկսում է նախորդ tween-ի հետ միաժամանակ, '-=0.2'-ը համընկնում է 0.2 վայրկյանով։ Timeline-ները կարելի է դադարեցնել, հետ պտտել, արագացնել և scrub անել, ինչը դրանք իդեալական է դարձնում intro-ների և scroll պատմությունների համար։ 2025-ից GSAP-ը և նրա բոլոր plugin-ները, ներառյալ SplitText-ը և ScrollTrigger-ը, անվճար են կոմերցիոն օգտագործման համար։ React-ում օգտագործեք useGSAP hook-ը @gsap/react-ից. այն selector-ները սահմանափակում է container-ով և unmount-ի ժամանակ հետ է շրջում բոլոր անիմացիաները։
"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 և SplitText
ScrollTrigger-ը անիմացիան կապում է scroll-ի դիրքի հետ։ Սահմանում եք trigger տարր և սկզբի ու վերջի կետեր, օրինակ՝ 'top 80%' (երբ տարրի վերին եզրը հասնում է viewport-ի 80%-ին)։ scrub: true-ի դեպքում անիմացիան հետևում է scrollbar-ին, pin: true-ի դեպքում տարրը ֆիքսված է մնում, մինչ timeline-ը խաղում է։ Մշակման ընթացքում օգտագործեք markers: true։ SplitText-ը տեքստը բաժանում է տողերի, բառերի կամ տառերի, որպեսզի կարողանաք դրանք stagger անել։ 2025-ի վերաշարադրված տարբերակն ավելի փոքր է, autoSplit-ով նորից է բաժանում resize-ի ժամանակ, կարող է տողերը փաթաթել mask-երի մեջ reveal էֆեկտների համար և ծնող տարրին ավելացնում է aria-label՝ բաժանված կտորները թաքցնելով screen reader-ներից։ Բաժանելուց առաջ միշտ սպասեք font-երի բեռնմանը, այլապես տողադարձերը սխալ կլինեն։
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%" },
}),
});
});
04Սահուն scroll Lenis-ով
Lenis-ը փոքր գրադարան է, որը սահունացնում է մկնիկի անիվի և trackpad-ի scroll-ը՝ պահպանելով բնիկ scroll-ի վարքը. էջը դեռ scroll է անում document-ը, ուստի position: sticky-ն, anchor-ները և հասանելիության գործիքները շարունակում են աշխատել։ Ինտեգրման հիմնական կետը ScrollTrigger-ն է. երկուսն էլ պետք է օգտագործեն նույն ժամացույցը և scroll-ի նույն դիրքը։ Lenis-ը շարժում եք GSAP-ի ticker-ից և ScrollTrigger-ին ասում եք թարմանալ Lenis-ի յուրաքանչյուր scroll իրադարձության ժամանակ։ Անջատեք GSAP-ի lag smoothing-ը, որպեսզի դրանք երբեք չշեղվեն իրարից։ Զգույշ եղեք ներդրված scroll տարածքների հետ (modal-ներ, dropdown-ներ). նշեք դրանք data-lenis-prevent-ով, որպեսզի բնիկ scroll անեն։ Reduced motion-ը հարգեք՝ այդ օգտատերերի համար Lenis-ն ընդհանրապես չգործարկելով։
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-ի հիմունքներ
Three.js-ը WebGL-ի (և այժմ նաև WebGPU-ի) վրա կառուցված գրադարան է բրաուզերում 3D-ի համար։ Յուրաքանչյուր տեսարանի պետք է չորս բան. Scene, որը պահում է օբյեկտները, Camera, որը նայում է դրան, Renderer, որը նկարում է canvas-ի վրա, և Mesh-եր, որոնք geometry (ձևը) գումարած material (ինչպես է այն երևում) են։ Material-ների մեծ մասի համար լույսեր են պետք։ Անիմացիայի ցիկլը թարմացնում է օբյեկտները և render է անում յուրաքանչյուր frame-ը։ Արտադրողականության համար pixel ratio-ն սահմանափակեք 2-ով, վերաօգտագործեք geometry-ները և material-ները, դադարեցրեք render-ը, երբ canvas-ը էկրանից դուրս է, և կոմպոնենտի unmount-ի ժամանակ կանչեք dispose() geometry-ների, material-ների և texture-ների վրա, այլապես GPU հիշողությունը կարտահոսի։ React նախագծերում React Three Fiber-ը նույն գաղափարները փաթաթում է կոմպոնենտների մեջ։
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);
});
06Արտադրողականություն. transform, will-change և layout thrash
«Կմկմացող» անիմացիաների մեծ մասն ունի երեք պատճառ։ Առաջին՝ layout հատկությունների անիմացիա. անցեք transform-ների (GSAP-ի x-ը, y-ը, scale-ը և rotation-ն արդեն transform են օգտագործում)։ Երկրորդ՝ layout thrash. կոդ, որը գրում է style, հետո կարդում offsetHeight կամ getBoundingClientRect, հետո նորից գրում՝ ստիպելով բրաուզերին մեկ frame-ի ներսում բազմիցս վերահաշվել layout-ը։ Նախ խմբով կատարեք բոլոր կարդալները, հետո բոլոր գրելները։ Երրորդ՝ չափազանց շատ շերտեր կամ հսկայական paint արվող տարածք։ will-change: transform-ը տարրը բարձրացնում է իր սեփական GPU շերտի, ինչն օգնում է անիմացիայի պատրաստվող տարրի դեպքում, բայց յուրաքանչյուր շերտ հիշողություն է արժենում, ուստի ավելացրեք այն քիչ առաջ և հեռացրեք հետո, երբեք՝ հարյուրավոր տարրերի վրա։ Պրոֆիլավորեք Chrome DevTools-ի Performance վահանակում. երկար մանուշակագույն (layout) և կանաչ (paint) բլոկները ցույց են տալիս խնդիրը։
- GSAP-ում անիմացիա արեք x, y, scale, rotation և autoAlpha։
- Նախ կարդացեք բոլոր չափումները, հետո գրեք style-երը։
- will-change-ն օգտագործեք խնայողաբար և ժամանակավորապես։
- Route-ի փոփոխության ժամանակ kill արեք ScrollTrigger-ները և tween-ները՝ արտահոսքերից խուսափելու համար։
07Reduced motion և հասանելիություն
Որոշ մարդկանց մոտ մեծ շարժումը, parallax-ը և zoom-ը գլխապտույտ կամ սրտխառնոց են առաջացնում։ Նրանք իրենց օպերացիոն համակարգում միացնում են «reduce motion», և բրաուզերը դա տրամադրում է որպես prefers-reduced-motion media query։ Հարգեք այն. պահեք fade-երը և գույնի փոփոխությունները, բայց հանեք մեծ շարժումները, parallax-ը, ինքնաշխատ ցիկլերը և scroll-ի «առևանգումը» smooth scroll-ով։ gsap.matchMedia()-ն թույլ է տալիս անիմացիաներ սահմանել ըստ պայմանի և ավտոմատ հետ է շրջում դրանք, երբ պայմանը փոխվում է։ Նույնքան կարևոր է՝ էջը պետք է ամբողջական լինի առանց JavaScript-ի։ CSS-ում թաքցված բովանդակությունը, որը սպասում է անիմացիային՝ բացահայտվելու համար, պետք է ունենա տեսանելի վերջնական վիճակ, եթե script-ը ձախողվի։ Երբեք անիմացիայով focus-ը մի տարեք, հինգ վայրկյանից ավելի շարժվող ամեն ինչ թողեք դադարեցվող (WCAG պահանջ) և խուսափեք վայրկյանում երեք անգամից ավելի թարթումից։
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 });
});
Օգտագործեք, երբ
- Բրենդային, գործակալության և արշավային կայքեր, որտեղ շարժումը պատմության մաս է։
- Հաջորդական intro-ներ, էջերի անցումներ և scroll-ով կառավարվող storytelling։
- Միկրո-ինտերակցիաներ, որոնք բացատրում են վիճակի փոփոխությունները, օրինակ՝ զամբյուղում ավելացնելը կամ վահանակ բացելը։
- Ինտերակտիվ 3D ապրանքի դիտումներ կամ խաղային տեսարաններ, երբ բրենդն արդարացնում է դրանց «քաշը»։
Մի՛ օգտագործեք, երբ
- Պարզ hover և focus վիճակներ. CSS transition-ներն ավելի թեթև են և բավարար։
- Տվյալներով ծանրաբեռնված dashboard-ներ և ադմին գործիքներ, որտեղ շարժումը դանդաղեցնում է մարդկանց։
- Smooth scroll բովանդակությամբ կամ ֆորմաներով ծանրաբեռնված հավելվածներում, որտեղ սպասվում է բնիկ scroll։
Այլընտրանքներ
- CSS transitions and keyframes
Զրո JavaScript, հիանալի է hover վիճակների և պարզ հայտնվելու էֆեկտների համար։
- CSS scroll-driven animations
animation-timeline: view()-ն և scroll()-ը աշխատում են main thread-ից դուրս. բրաուզերների աջակցությունը դեռ աճում է, ուստի fallback պահեք։
- View Transitions API
Բնիկ անիմացիոն անցումներ DOM վիճակների և էջերի միջև, այժմ օգտագործելի ժամանակակից բրաուզերներում։
- Motion (formerly Framer Motion)
Դեկլարատիվ React անիմացիա layout անիմացիաներով. հարմար է կոմպոնենտային հավելվածներում։
Լավագույն պրակտիկաներ
- Անիմացիա արեք transform և opacity. խուսափեք layout հատկություններից։
- Օգտագործեք timeline-ներ defaults-ով, որպեսզի շարժումը հետևողական և հեշտ կարգավորելի մնա։
- Unmount-ի ժամանակ անիմացիաները սահմանափակեք և մաքրեք useGSAP-ով կամ context.revert()-ով։
- Lenis-ը և ScrollTrigger-ը կապեք նույն ticker-ին։
- Հարգեք prefers-reduced-motion-ը gsap.matchMedia()-ով։
- Յուրաքանչյուր անիմացիոն տարրի տվեք տեսանելի վերջնական վիճակ առանց JavaScript-ի։
- Three.js տեսարանները lazy-load արեք և unmount-ի ժամանակ dispose արեք GPU ռեսուրսները։
Տարածված սխալներ
- Անիմացիա անել top, left կամ width և զարմանալ, թե ինչու է հեռախոսներում կմկմում։
- CSS-ում բովանդակությունը թաքցնել opacity: 0-ով, այնպես որ այն անտեսանելի է մնում, եթե script-ը ձախողվի։
- Մոռանալ kill անել ScrollTrigger-ները client-side նավիգացիայի ժամանակ, ինչը հանգեցնում է սխալ դիրքերի և արտահոսքերի։
- Տեքստը բաժանել մինչև վեբ font-երի բեռնվելը, այնպես որ տողադարձերը տեղաշարժվում են անիմացիայի սկսվելուց հետո։
- will-change դնել ամեն ինչի վրա, ինչն ավելի շատ հիշողություն է օգտագործում և կարող է ամեն ինչ դանդաղեցնել։
Խորը վերլուծություն
08Compositor layers՝ ինչ է իրականում անում GPU-ն
Երբ անիմացնում եք transform կամ opacity, բրաուզերը կարող է էլեմենտը տեղաշարժել compositor thread-ում՝ առանձին main thread-ից, որտեղ աշխատում է JavaScript-ը։ Դրա համար էլեմենտին պետք է սեփական layer՝ GPU հիշողության մեջ պահվող bitmap։ Բրաուզերներն ավտոմատ promote են անում էլեմենտները CSS transitions-ի և Web Animations API-ի transform-ների ժամանակ, կամ երբ դնում եք will-change: transform։ Յուրաքանչյուր layer արժե մոտավորապես լայնություն անգամ բարձրություն անգամ չորս բայթ՝ բազմապատկած device pixel ratio-ի քառակուսիով, ուստի 3x հեռախոսի վրա մեկ ամբողջ էկրանի layer-ն արդեն տասը մեգաբայթից ավելի է։ Promote արեք մի քանի հարյուր քարտ, և մոբայլ բրաուզերները կսկսեն frame-ներ բաց թողնել կամ նույնիսկ վերաբեռնել tab-ը։ Ուշադրություն դարձրեք կարևոր տարբերությանը. transform-ի CSS և Web Animations API անիմացիաները կարող են շարունակել աշխատել նույնիսկ երբ main thread-ը բլոկավորված է, բայց JavaScript-ով կառավարվող անիմացիաները, այդ թվում GSAP-ը, ամեն frame-ը հաշվում են main thread-ում, ուստի long task-երը դրանք կակազեցնում են։ Ստուգեք, թե ինչն է promote արված, DevTools-ի Layers վահանակով և Rendering tab-ի layer borders-ով։
- Layer-ի հիշողությունն աճում է մակերեսի և pixel ratio-ի քառակուսու հետ, ոչ միայն էլեմենտների քանակի։
- Promote արված layer-ի հետ վերադրվելը կարող է ստիպել մյուս էլեմենտներին նույնպես layer դառնալ (layer explosion)։
- Այն էֆեկտների համար, որոնք պետք է դիմանան զբաղված main thread-ին, նախընտրեք CSS կամ Web Animations API։
09Բարձր հաճախականության շարժում՝ quickTo, overwrite և ticker
Որոշ շարժումներ թարմացվում են վայրկյանում տասնյակ անգամներ. cursor follower, magnetic button, pointer-ին արձագանքող parallax։ Յուրաքանչյուր pointermove-ի ժամանակ նոր gsap.to() ստեղծելը օբյեկտներ է հատկացնում և ստեղծում իրար հետ կռվող tween-ներ։ gsap.quickTo()-ն յուրաքանչյուր property-ի համար կառուցում է մեկ վերաօգտագործելի ֆունկցիա, որը պարզապես վերաթիրախավորում է մեկ tween, ինչը շատ ավելի էժան է։ Երբ մի քանի tween կարող են անիմացնել նույն property-ն, դրեք overwrite: 'auto', որպեսզի GSAP-ը սպանի հին tween-ների միայն վերադրվող մասը։ GSAP-ի ամբողջ անիմացիան աշխատում է requestAnimationFrame-ի վրա կառուցված մեկ ticker-ով. ձեր սեփական per-frame տրամաբանությունը միացրեք gsap.ticker.add()-ին՝ երկրորդ loop սկսելու փոխարեն, որպեսզի ամեն ինչ համաժամանակյա մնա և միասին դադարի։ GSAP-ը նաև կիրառում է lag smoothing երկար frame-ից հետո, որպեսզի շարժումը չթռչի։ Այս handler-ների ներսում երբեք layout մի կարդացեք. էլեմենտների չափերը cache արեք resize-ի ժամանակ ResizeObserver-ով և գրեք միայն transform-ներ։
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 case-եր, որոնք կոտրվում են production-ում
ScrollTrigger-ը հաշվում է start և end դիրքերը և cache է անում դրանք, ուստի այն ամենը, ինչ հետագայում փոխում է էջի բարձրությունը, ստիպում է trigger-ներին աշխատել սխալ տեղում. առանց չափերի նկարներ, web font-երի փոխարինում, client-ում fetch արված կոնտենտ, բացվող accordion-ներ։ Տեղ պահեք media-ի համար և նման փոփոխություններից հետո կանչեք ScrollTrigger.refresh(). այն արդեն աշխատում է load-ի և resize-ի ժամանակ։ Ստեղծեք trigger-ները այն հերթականությամբ, որով դրանք հայտնվում են էջում, կամ դրեք refreshPriority, որովհետև բաժնի pin-ը ավելացնում է spacing, որը տեղաշարժում է ներքևի ամեն ինչ։ Հեռախոսներում address bar-ի երևալն ու թաքնվելը փոխում է viewport-ի չափը. ScrollTrigger.config({ ignoreMobileResize: true })-ը բաց է թողնում refresh-ները այդ ուղղահայաց resize-ների համար, իսկ svh միավորներով բարձրությունները կայուն են մնում։ Transform ունեցող parent-ի ներսում pin-ը սխալ է աշխատում, որովհետև position: fixed-ը դառնում է հարաբերական transform ունեցող ցանկացած ancestor-ի նկատմամբ. փոխարենը pin արեք արտաքին բաժինը։ Single-page նավիգացիայում սպանեք հին trigger-ները նախքան նորերը ստեղծելը, այլապես նախորդ էջի դիրքերը կթափանցեն հաջորդ էջ։
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 մեծ մասշտաբում՝ draw calls, shaders և WebGPU
3D տեսարանը սովորաբար սահմանափակված է draw call-երով և shader-ների աշխատանքով, ոչ թե եռանկյունների քանակով։ Սեփական material ունեցող յուրաքանչյուր mesh առնվազն մեկ draw call է, ուստի հազար միանման օբյեկտ որպես առանձին mesh-եր արժեն հազար call. մեկ InstancedMesh-ը դրանք բոլորը նկարում է մեկ call-ով՝ յուրաքանչյուր instance-ի համար մեկ matrix-ով։ Միավորեք ստատիկ geometry-ն և կիսեք material-ները, որպեսզի GPU-ն ավելի հազվադեպ փոխի վիճակը։ Առաջին frame-ը, որն օգտագործում է նոր material, compile է անում դրա shader-ը main thread-ում, ինչը կարող է էջը սառեցնել հարյուրավոր միլիվայրկյաններով. նախքան տեսարանը ցույց տալը precompile արեք renderer.compileAsync()-ով։ Այն տեսարանների համար, որոնք փոխվում են միայն ինտերակցիայի ժամանակ, ռենդերեք ըստ պահանջի՝ ամեն frame-ի փոխարեն (React Three Fiber-ում՝ frameloop='demand')։ Հետևեք renderer.info-ին՝ live draw call-երի, եռանկյունների, geometry-ների և texture-ների համար. նավիգացիաների ընթացքում աճող թվերը նշանակում են leak։ Three.js-ը նաև ունի WebGPURenderer, որը fallback է անում WebGL 2-ին և հնարավորություն տալիս compute shader-ներ, բայց պարզ WebGL-ը մնում է ամենաապահով ընտրությունը, երբ կարևոր է սարքերի լայն աջակցությունը։
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);
Իրական իրավիճակներ
Pin արված բաժինը iPhone-ներում թռչում է և շեղվում
Խնդիր
Pin արված storytelling բաժինն աշխատում է desktop-ում, բայց iPhone-ներում թռչում է, երբ address bar-ը փլվում է, և երբեմն շատ վաղ է unpin լինում նկարների բեռնվելուց հետո։
Ինչպես լուծել
Նախ վերարտադրեք իրական սարքի վրա Safari-ի remote Web Inspector-ով և markers: true-ով։ Հետո ուղղեք պատճառները մեկ առ մեկ։ Յուրաքանչյուր նկարի տվեք width և height կամ aspect-ratio, որպեսզի ուշ բեռնումները չփոխեն էջի բարձրությունը։ Կանչեք ScrollTrigger.refresh() document.fonts.ready-ից հետո և client-ում fetch արված կոնտենտի ռենդերից հետո։ Դրեք ScrollTrigger.config({ ignoreMobileResize: true }) և ամբողջ բարձրության պանելների չափը տվեք 100svh-ով՝ 100vh-ի փոխարեն։ Ստուգեք, որ pin արված էլեմենտի ոչ մի ancestor չունենա transform, filter կամ will-change, որոնք կոտրում են fixed positioning-ը։ Վերջում prefers-reduced-motion-ի դեպքում gsap.matchMedia()-ով ամբողջությամբ բաց թողեք pin-ը և ցույց տվեք քայլերը որպես սովորական ուղղահայաց ցուցակ։
INP-ն վատացավ նոր անիմացված hero-ի թողարկումից հետո
Խնդիր
Field INP-ն միջին կարգի Android հեռախոսներում մոտ 150 ms-ից բարձրացավ 300 ms-ից ավելի՝ split text-ով, բազմաթիվ scroll էֆեկտներով և փոքր 3D տեսարանով նոր hero-ից հետո։ Անիմացիան ինքնին սահուն է թվում։
Ինչպես լուծել
Գրանցեք trace DevTools-ում 4x CPU throttling-ով և բեռնման ժամանակ կտտացրեք տարբեր տեղեր. փնտրեք long task-եր, որոնք վերադրվում են ինտերակցիաների հետ։ Սովորական մեղավորներն են՝ SplitText-ը, որը մեկ task-ում բաժանում է հարյուրավոր նիշեր, միանգամից ստեղծվող տասնյակ ScrollTrigger-ներ, shader-ների compile-ը առաջին ռենդերի ժամանակ, layout կարդացող pointermove handler-ներ։ Բաժանեք ըստ բառերի կամ տողերի՝ նիշերի փոխարեն, fold-ից ներքև էֆեկտները ստեղծեք միայն երբ IntersectionObserver-ն ասում է, որ դրանք մոտ են, և setup-ի մասերի միջև yield արեք scheduler.yield()-ով, որտեղ աջակցվում է։ Precompile արեք 3D տեսարանը compileAsync-ով և lazy-load արեք Three.js-ը էջի ինտերակտիվ դառնալուց հետո։ Հաստատեք field-ում web-vitals attribution build-ով, որը նշում է դանդաղ ինտերակցիան և սկրիպտը։
Հիշողությունն աճում է յուրաքանչյուր client-side նավիգացիայի հետ
Խնդիր
Next.js պորտֆոլիոյում տասը case study այցելելուց հետո tab-ը օգտագործում է մեկ գիգաբայթից ավելի, և scroll-ը կակազում է։ Յուրաքանչյուր էջ ունի Three.js canvas և scroll անիմացիաներ։
Ինչպես լուծել
Վերցրեք heap snapshot-ներ DevTools-ի Memory վահանակում մի քանի նավիգացիայից առաջ և հետո և համեմատեք detached DOM node-երը և պահված WebGL օբյեկտները։ Unmount-ի ժամանակ revert արեք GSAP-ը useGSAP-ի միջոցով, կանչեք lenis.destroy(), եթե յուրաքանչյուր էջ Lenis է ստեղծում, կանգնեցրեք loop-ը renderer.setAnimationLoop(null)-ով, անցեք տեսարանով՝ dispose անելու geometry-ները, material-ները և texture-ները, հետո կանչեք renderer.dispose()։ Անջատեք ResizeObserver-ները և հեռացրեք window listener-ները։ Բրաուզերները սահմանափակում են live WebGL context-ների քանակը և զգուշացմամբ հեռացնում ամենահինը, ուստի ավելի լավ ճարտարապետությունը root layout-ում մեկ մշտական canvas է, որը route-երի միջև փոխում է տեսարանները՝ յուրաքանչյուր էջի համար նոր renderer ստեղծելու փոխարեն։
Արագ ուղեցույց
- Էժան վերաթիրախավորել մեկ property
const xTo = gsap.quickTo(el, "x", { duration: 0.3 }); xTo(120);- Կանգնեցնել վերադրվող tween-ների կռիվը
gsap.to(el, { x: 100, overwrite: "auto" });- Վերահաշվել trigger-ների դիրքերը
ScrollTrigger.refresh();- Անտեսել address bar-ի resize-ները մոբայլում
ScrollTrigger.config({ ignoreMobileResize: true });- Դանդաղեցնել բոլոր անիմացիաները debug-ի համար
gsap.globalTimeline.timeScale(0.2);- Սպանել բոլոր trigger-ները route-ի փոփոխության ժամանակ
ScrollTrigger.getAll().forEach((t) => t.kill());- Կանգնեցնել Three.js render loop-ը
renderer.setAnimationLoop(null);- Precompile անել shader-ները տեսարանը ցույց տալուց առաջ
await renderer.compileAsync(scene, camera);- Հաշվել draw call-երը մեկ frame-ում
renderer.info.render.calls- Ռենդերել միայն երբ ինչ-որ բան փոխվում է (R3F)
<Canvas frameloop="demand">
Իմ իրական նախագծերում
- GSAP օգտագործում եմ մի քանի Next.js նախագծերում, այդ թվում՝ ռեստորանային ցանցի պատվերների կայքում, կենդանիների ապրանքների առցանց խանութում, գինեգործարանի խանութում և թվային գործակալության կայքում։
- Գործակալության կայքի վերաշարադրման ժամանակ frontend-ը համատեղում էր GSAP-ը three.js մինի-խաղի, particle cursor-ի և Lottie անիմացիաների հետ՝ dark mode-ի կողքին։
- Գինեգործարանի direct-to-consumer կայքը, որի վրա աշխատել եմ, ներառում է Three.js-ով կառուցված 3D տեսարան։
- Այս կայքը օգտագործում է GSAP՝ ScrollTrigger-ով, SplitText-ով և Flip-ով, գումարած Lenis, որոնք կառավարվում են data attribute-ներով, որպեսզի server component-ները երբեք GSAP import չանեն, reduced motion-ը հարգվում է, և յուրաքանչյուր էֆեկտ ունի տեսանելի վերջնական վիճակ։
Հարցեր և պատասխաններ
Q1Ինչու՞ պետք է անիմացիա անել transform և opacity, այլ ոչ թե top կամ width։
Rendering pipeline-ի պատճառով։ top-ի, left-ի, width-ի կամ margin-ի փոփոխությունը փոխում է երկրաչափությունը, ուստի բրաուզերը պետք է յուրաքանչյուր frame-ում վերահաշվի layout-ը, վերանկարի պիքսելները և հետո composite անի, հաճախ՝ շատ տարրերի համար։ Transform-ը և opacity-ն սովորաբար կարող է մշակել compositor-ը GPU-ի վրա՝ առանց layout-ի կամ paint-ի, ուստի դրանք սահուն են մնում նույնիսկ միջին դասի հեռախոսներում։ Գործնականում տարրերը տեղաշարժում եմ translate-ով և scale-ով, fade-ն անում opacity-ով, իսկ եթե իսկապես չափի փոփոխություն է պետք, անիմացիա եմ անում scale և ուղղում բովանդակությունը, կամ օգտագործում FLIP-ի նման տեխնիկա, որը մեկ անգամ չափում է սկզբի և վերջի layout-ները և տարբերությունն անիմացիա անում որպես transform։
Q2Ի՞նչ է layout thrashing-ը, և ինչպե՞ս եք դրանից խուսափում։
Layout thrashing-ը տեղի է ունենում, երբ կոդը նույն frame-ի ներսում հերթափոխում է style գրելը և layout արժեքներ կարդալը։ Օրինակ՝ ցիկլ, որը սահմանում է տարրի height-ը, հետո կարդում հաջորդի offsetHeight-ը։ Յուրաքանչյուր կարդալ ստիպում է բրաուզերին սինխրոն վերահաշվել layout-ը, որպեսզի կարողանա ճիշտ թիվ վերադարձնել։ Շատ տարրերի դեպքում դա դառնում է տասնյակ ստիպողական layout-ներ մեկ frame-ում։ Լուծումը խմբավորումն է. նախ կարդացեք բոլոր չափումները, պահեք դրանք, հետո կիրառեք բոլոր գրելները։ GSAP-ի նման անիմացիոն գրադարաններն արդեն ներքուստ խմբավորում են, իսկ requestAnimationFrame-ն օգնում է պլանավորել գրելները։ DevTools-ի Performance վահանակը նշում է forced reflow-ները, ինչը հեշտացնում է դրանք գտնելը։
Q3Ինչպե՞ս ճիշտ օգտագործել GSAP-ը React-ում և Next.js-ում։
GSAP-ը դիպչում է DOM-ին, ուստի աշխատում է միայն client component-ներում։ Օգտագործում եմ useGSAP hook-ը @gsap/react-ից. այն աշխատում է DOM-ի պատրաստ լինելուց հետո, selector string-երը սահմանափակում է container ref-ով, այնպես որ .title-ը համընկնում է միայն այդ կոմպոնենտի ներսում, և կոմպոնենտի unmount-ի ժամանակ ավտոմատ հետ է շրջում յուրաքանչյուր tween և ScrollTrigger։ Դա կարևոր է Next.js-ում, որտեղ client-side նավիգացիան այլապես հնացած trigger-ներ կթողներ։ Ավելի ուշ ստեղծվող, իրադարձություններով կառավարվող անիմացիաները փաթաթում եմ contextSafe-ով։ Նաև GSAP-ը ամբողջությամբ դուրս եմ պահում server component-ներից, օրինակ՝ տարրերը նշելով data attribute-ներով և թողնելով, որ մեկ client-side provider կցի էֆեկտները։
Q4Ինչպե՞ս եք հարգում prefers-reduced-motion-ը։
Դրան վերաբերվում եմ որպես առանձին motion design-ի, ոչ թե անջատիչի։ gsap.matchMedia()-ով լիարժեք անիմացիաները գրանցում եմ no-preference-ի ներքո, իսկ ավելի պարզ հավաքածու՝ reduce-ի ներքո. կարճ fade-եր և գույնի փոփոխություններ շարժման փոխարեն, ոչ մի parallax, ոչ մի pinned scroll տեսարան, ոչ մի smooth scroll։ matchMedia-ն նաև ավտոմատ հետ է շրջում անիմացիաները, եթե օգտատերը փոխում է կարգավորումը, մինչ էջը բաց է։ CSS-ում տարրերը շարժող transition-ները փաթաթում եմ no-preference media query-ի մեջ։ Եվ անկախ շարժման կարգավորումներից, բովանդակությունը պետք է լիովին տեսանելի և օգտագործելի լինի, եթե JavaScript-ը դանդաղ է կամ անջատված։
Q5Smooth scroll-ը վնասու՞մ է հասանելիությանը կամ արտադրողականությանը։
Կարող է, եթե վատ է արված։ Հին գրադարանները բնիկ scroll-ը փոխարինում էին transform արված container-ով, ինչը կոտրում էր position: sticky-ն, anchor-ները, էջում որոնումը և օժանդակ գործիքները։ Lenis-ը պահպանում է document-ի բնիկ scroll-ը և միայն սահունացնում է մուտքը, ուստի դրանք շարունակում են աշխատել։ Այնուամենայնիվ, այն անջատում եմ prefers-reduced-motion-ի համար, ներդրված scroll տարածքներին թույլ եմ տալիս բնիկ scroll անել data-lenis-prevent-ով և անխախտ պահում եմ ստեղնաշարով scroll-ը։ Արտադրողականության առումով Lenis-ն ինքնին թեթև է. իրական գինն այն է, ինչ աշխատում է յուրաքանչյուր scroll-ի ժամանակ, ուստի scroll-ին կապված էֆեկտները պետք է անիմացիա անեն միայն transform-ներ և օգտագործեն ScrollTrigger՝ custom scroll listener-ների փոխարեն։ Հավելվածանման կամ ֆորմաներով ծանրաբեռնված էջերում պարզապես չեմ օգտագործում այն։
Q6Ինչպե՞ս թույլ չտալ, որ Three.js տեսարանը վնասի էջի արտադրողականությանը։
Նախ՝ բեռնեք այն lazy. Three.js-ը import արեք դինամիկ միայն այն ժամանակ, երբ canvas-ը մոտ է viewport-ին, որպեսզի այն երբեք չուշացնի հիմնական բովանդակությունը կամ LCP-ն։ Device pixel ratio-ն սահմանափակեք 2-ով, պոլիգոնների քանակը և texture-ների չափերը պահեք խելամիտ սահմաններում և օգտագործեք սեղմված ձևաչափեր, օրինակ՝ KTX2 texture-ներ և Draco կամ Meshopt geometry։ Դադարեցրեք render ցիկլը, երբ canvas-ը էկրանից դուրս է կամ tab-ը թաքնված է։ Geometry-ները և material-ները կիսեք mesh-երի միջև։ Unmount-ի ժամանակ dispose արեք geometry-ները, material-ները, texture-ները և renderer-ը՝ GPU հիշողությունն ազատելու համար։ Վերջապես, reduced motion-ի և ցածր հզորության սարքերի համար առաջարկեք ստատիկ պատկերի fallback։
Սենիոր մակարդակի հարցեր
S1GSAP, CSS անիմացիաներ, թե՞ Web Animations API. ինչպե՞ս եք որոշում յուրաքանչյուր էֆեկտի համար։
Որոշում եմ ըստ այն բանի, թե ինչ է պետք էֆեկտին։ Պարզ վիճակի փոփոխությունները՝ hover, focus, պանելների բացում կամ փակում, օգտագործում են CSS transitions. զրո JavaScript, և transform-ի ու opacity-ի անիմացիաները կարող են աշխատել compositor-ում նույնիսկ երբ main thread-ը զբաղված է։ Scroll-ի դիրքին կապված մուտքի էֆեկտները կարող են օգտագործել CSS scroll-driven animations, որտեղ աջակցությունը թույլ է տալիս, ստատիկ fallback-ով։ Web Animations API-ն օգտագործում եմ, երբ ինձ պետք է JavaScript վերահսկողություն, օրինակ՝ reverse անել կամ սպասել ավարտին, առանց գրադարանի։ GSAP-ն արդարացնում է իր քաշը հաջորդական timeline-երի, scrub և pin արված scroll տեսարանների, text splitting-ի, SVG morphing-ի և cross-browser հետևողականության համար։ Դրա արժեքը main thread-ի աշխատանքն է ամեն frame-ում և bundle-ի չափը, ուստի plugin-ները բեռնում եմ միայն որտեղ օգտագործվում են։ Տիպիկ կայքը խառնում է երեքն էլ, և կանոնն այն ամենաթեթև գործիքն է, որը հուսալիորեն անում է գործը։
S2Ինչպե՞ս եք կառուցում անիմացիան մեծ Next.js կայքում, որպեսզի server component-ները մնան առանց անիմացիայի։
Առանձնացնում եմ նկարագրությունը կատարումից։ Server component-ները միայն ռենդերում են markup՝ data attribute-ներով, օրինակ՝ data-anim='fade-up' կամ data-anim-pin, գումարած իրենց վերջնական տեսանելի վիճակը, ուստի էջն ամբողջական է առանց JavaScript-ի։ Layout-ում mount արված մեկ client provider hydration-ից հետո փնտրում է այդ attribute-ները, յուրաքանչյուր անուն քարտեզագրում է փոքր registry-ի factory ֆունկցիային և անիմացիաները ստեղծում է էջով սահմանափակված GSAP context-ի ներսում։ Route-ի փոփոխության ժամանակ այն revert է անում այդ context-ը, ինչը մեկ կանչով սպանում է tween-ներն ու trigger-ները, հետո փնտրում է նոր էջում։ Ծանր մասերը, ինչպես Three.js-ը կամ SplitText-ը, dynamic import են արվում միայն երբ դրանց կարիք ունեցող էլեմենտ գոյություն ունի։ gsap.matchMedia()-ն փաթաթում է ամեն ինչ reduced motion-ի և breakpoint-ների համար։ Փոխզիջումը մի փոքր անուղղակիությունն է, բայց bundle-ները փոքր են մնում, server component-ները՝ մաքուր, և յուրաքանչյուր էֆեկտ հետևողականորեն մաքրվում է։
S3Ինչպե՞ս եք սահմանում և պարտադրում performance budget շարժումով հարուստ էջերի համար։
Budget-ները սահմանում եմ օգտատիրոջ տերմիններով. սահուն frame-եր միջին կարգի Android հեռախոսում, անիմացիայի setup-ի պատճառով 50 ms-ից երկար ոչ մի long task, LCP և INP անփոփոխ՝ համեմատած առանց շարժման էջի հետ, և JavaScript budget անիմացիայի կոդի համար, օրինակ՝ GSAP core գումարած միայն օգտագործվող plugin-ները։ Պարտադրումն ունի երեք շերտ։ Development-ում profile եմ անում CPU throttling-ով և ստուգում frame-երն ու long task-երը Performance վահանակում։ CI-ում Lighthouse-ը հիմնական էջերի վրա հետևում է Total Blocking Time-ին, իսկ bundle-size սահմանները ձախողում են build-ը, երբ անիմացիայի կոդն աճում է։ Production-ում web-vitals գրադարանը հաղորդում է INP-ն attribution-ով, ուստի regression-ը ցույց է տալիս պատասխանատու սկրիպտը։ Երբ budget-ը գերազանցվում է, պարզեցնում եմ էֆեկտը կամ տեղափոխում CSS՝ արժեքն ընդունելու փոխարեն, որովհետև շարժումը երբեք չպետք է էջն օգտագործելու համար ավելի դանդաղ դարձնի։
Բառարան
- Compositor thread
- Բրաուզերի thread, որը layer-ները միավորում է վերջնական պատկերի մեջ. այն կարող է layer-ներ տեղաշարժել առանց JavaScript-ին սպասելու։
- Layer promotion
- Էլեմենտին սեփական GPU bitmap տալը, որպեսզի այն էժան տեղաշարժվի կամ մարի՝ հիշողության հաշվին։
- Main thread
- Միակ thread-ը, որտեղ աշխատում են JavaScript-ը, style-ը, layout-ն ու paint-ը. այստեղ long task-երը jank են առաջացնում։
- quickTo
- GSAP helper, որը վերադարձնում է վերաօգտագործելի ֆունկցիա մեկ property-ի tween-ը վերաթիրախավորելու համար. իդեալական է pointer-ով շարժման համար։
- Lag smoothing
- GSAP-ի ճշգրտումը երկար frame-ից հետո, որպեսզի անիմացիաները սահուն շարունակվեն՝ առաջ թռչելու փոխարեն։
- Draw call
- CPU-ից մեկ հրաման, որը GPU-ին ասում է նկարել geometry-ի խմբաքանակ մեկ material-ով։
- InstancedMesh
- Three.js օբյեկտ, որը մեկ geometry-ի և material-ի բազմաթիվ պատճեններ նկարում է մեկ draw call-ով։
- Shader compilation
- Material-ի GPU ծրագրի վերածումը մեքենայական կոդի. տեղի է ունենում առաջին օգտագործման ժամանակ և կարող է սառեցնել frame-ը։
- WebGPU
- Ժամանակակից բրաուզերային գրաֆիկայի և հաշվարկների API, որը փոխարինում է WebGL-ին՝ ավելի ցածր overhead-ով և compute shader-ներով։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։