רקעים אינטראקטיביים, חלקיקים וסצנות תלת-ממד הם טרנד, אבל יישום שלהם דרך DOM או SVG נתקל בתקרת ביצועים: 60 FPS מוחזק רק עד מאה אובייקטים. Canvas, לעומת זאת, יכול לעבד אלפי חלקיקים ללא פיגור — אבל רק עם ארכיטקטורה נכונה. לדוגמה, לקוח רצה רקע של 3000 חלקיקים ב-React — DOM הוריד את ה-FPS ל-5. כתבנו אותו מחדש על Canvas עם מאגר חלקיקים והשגנו 60 FPS יציב במובייל.
אנו מיישמים אנימציות Canvas במפתח מלא: מחלקיקים פשוטים ועד סצנות WebGL עם shaders. עלות הפיתוח השקופה מחושבת לפי מורכבות. נאמוד את הפרויקט שלך תוך יום אחד — צור קשר לייעוץ.
אילו בעיות אנו פותרים
ביצועים נמוכים עם אובייקטים רבים
אנימציות DOM מתחילות לפגר ב-100–200 אלמנטים. Canvas מצייר מחדש הכל במעבר אחד, אבל דורש ניהול זיכרון ידני ושימוש חוזר באובייקטים. אנו מייעלים מאגרי חלקיקים, מונעים איסוף זבל, ונלחמים בציור N+1. אנו מבטיחים שלאחר אופטימיזציה האנימציה תרוץ ב-60 FPS על כל המכשירים היעד.
פיקסלים מטושטשים במסכי Retina
Canvas סטנדרטי מצייר בפיקסלי CSS — ב-Retina התמונה נראית מטושטשת. אנו מכפילים את הגודל ב-devicePixelRatio ומשנים את קנה המידה של הקונטקסט.
מורכבות אינטגרציה עם React/Vue
שימוש פשוט ב-canvas גורם לחוסר התאמה ב-hydration ולאובדן מצב. עטפנו את המנוע ב-useCanvas hooks עם ניהול מחזור חיים תקין.
זיכרון לא מבוקר באנימציות
ללא ניהול אובייקטים נכון, הזיכרון גדל וגורם להקפאות GC. אנו משתמשים במאגרי אובייקטים ונמנעים מהקצאות בלולאת הרינדור.
אילו ביצועים Canvas מספק
Canvas מהיר פי 10–50 מ-SVG על אלפי אובייקטים בשל היעדר תקורה של DOM. אנו משיגים 60 FPS יציב באמצעות:
-
requestAnimationFrameעם השהיה אוטומטית בטאבים לא פעילים - הגבלת Delta-time (מקסימום 100 אלפיות שנייה)
- הפחתת GC מאולצת באמצעות שימוש חוזר באובייקטים
השוואה בין Canvas ל-SVG ל-DOM animation:
| מאפיין | Canvas | SVG | DOM animation |
|---|---|---|---|
| מקסימום אובייקטים | 50,000+ | 1,000 | 500 |
| אינטראקטיביות | מערכת מותאמת אישית | מבוססת אירועים | מבוססת אירועים |
| טרנספורמציות | מותאם אישית | מאפייני CSS | מאפייני CSS |
| תמיכה ב-Retina | דרך DPR | אוטומטי | אוטומטי |
אופטימיזציה ל-Retina ולמכשירים ניידים כלולה במחיר.
איך אנחנו עושים את זה: טכנולוגיות ודוגמאות
אנו משתמשים ב-TypeScript, React 18, Three.js r150. תבניות מפתח: Repository, BFF לחלקיקים, לולאת רינדור עם הפרדת עדכון/ציור. קוד המנוע מוכן ל-tree-shaking, ומאפשר לכלול רק את המודולים הדרושים ב-bundle הסופי.
מנוע Canvas בסיסי
// lib/canvas-engine.ts
export interface AnimationContext {
canvas: HTMLCanvasElement
ctx: CanvasRenderingContext2D
width: number
height: number
dpr: number // device pixel ratio
dt: number // delta time в секундах
}
export type RenderFn = (context: AnimationContext) => void
export class CanvasEngine {
private canvas: HTMLCanvasElement
private ctx: CanvasRenderingContext2D
private dpr: number
private rafId: number | null = null
private lastTime: number = 0
private renderFn: RenderFn
constructor(canvas: HTMLCanvasElement, renderFn: RenderFn) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')!
this.dpr = window.devicePixelRatio || 1
this.renderFn = renderFn
this.resize()
}
resize() {
const { canvas, dpr } = this
const rect = canvas.getBoundingClientRect()
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
this.ctx.scale(dpr, dpr)
}
start() {
this.lastTime = performance.now()
this.tick(this.lastTime)
}
stop() {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId)
this.rafId = null
}
}
private tick = (timestamp: number) => {
const dt = Math.min((timestamp - this.lastTime) / 1000, 0.1)
this.lastTime = timestamp
const rect = this.canvas.getBoundingClientRect()
this.renderFn({
canvas: this.canvas,
ctx: this.ctx,
width: rect.width,
height: rect.height,
dpr: this.dpr,
dt,
})
this.rafId = requestAnimationFrame(this.tick)
}
} React hook useCanvas
// hooks/useCanvas.ts
import { useEffect, useRef } from 'react'
import { CanvasEngine, RenderFn } from '../lib/canvas-engine'
export function useCanvas(renderFn: RenderFn) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<CanvasEngine | null>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const engine = new CanvasEngine(canvas, renderFn)
engineRef.current = engine
engine.start()
const handleResize = () => engine.resize()
window.addEventListener('resize', handleResize)
return () => {
engine.stop()
window.removeEventListener('resize', handleResize)
}
}, [renderFn])
return canvasRef
} מערכת חלקיקים עם פיזיקה
// lib/particle-system.ts
interface Particle {
x: number
y: number
vx: number
vy: number
radius: number
color: string
life: number
maxLife: number
}
export class ParticleSystem {
private particles: Particle[] = []
private readonly maxParticles: number
constructor(maxParticles = 500) {
this.maxParticles = maxParticles
}
emit(x: number, y: number, count = 5) {
for (let i = 0; i < count; i++) {
if (this.particles.length >= this.maxParticles) break
const angle = Math.random() * Math.PI * 2
const speed = 50 + Math.random() * 150
this.particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 100,
radius: 2 + Math.random() * 4,
color: `hsl(${200 + Math.random() * 60}, 80%, 60%)`,
life: 1,
maxLife: 0.8 + Math.random() * 0.8,
})
}
}
update(dt: number) {
const gravity = 300
this.particles = this.particles.filter(p => {
p.x += p.vx * dt
p.y += p.vy * dt
p.vy += gravity * dt
p.vx *= 0.99
p.life -= dt / p.maxLife
return p.life > 0
})
}
draw(ctx: CanvasRenderingContext2D) {
for (const p of this.particles) {
ctx.save()
ctx.globalAlpha = p.life * p.life
ctx.fillStyle = p.color
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius * p.life, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
}
}קומפוננטת דוגמה: // lib/canvas-engine.ts export interface AnimationContext { canvas: HTMLCanvasElement ctx: CanvasRenderingContext2D width: number height: number dpr: number // device pixel ratio dt: number // delta time в секундах } export type RenderFn = (context: AnimationContext) => void export class CanvasEngine { private canvas: HTMLCanvasElement private ctx: CanvasRenderingContext2D private dpr: number private rafId: number | null = null private lastTime: number = 0 private renderFn: RenderFn constructor(canvas: HTMLCanvasElement, renderFn: RenderFn) { this.canvas = canvas this.ctx = canvas.getContext('2d')! this.dpr = window.devicePixelRatio || 1 this.renderFn = renderFn this.resize() } resize() { const { canvas, dpr } = this const rect = canvas.getBoundingClientRect() canvas.width = rect.width * dpr canvas.height = rect.height * dpr this.ctx.scale(dpr, dpr) } start() { this.lastTime = performance.now() this.tick(this.lastTime) } stop() { if (this.rafId !== null) { cancelAnimationFrame(this.rafId) this.rafId = null } } private tick = (timestamp: number) => { const dt = Math.min((timestamp - this.lastTime) / 1000, 0.1) this.lastTime = timestamp const rect = this.canvas.getBoundingClientRect() this.renderFn({ canvas: this.canvas, ctx: this.ctx, width: rect.width, height: rect.height, dpr: this.dpr, dt, }) this.rafId = requestAnimationFrame(this.tick) } } — מערכת חלקיקים אינטראקטיבית עם כבידה, פליטה אוטומטית ולחיצת עכבר.
מידע נוסף על shaders
לאפקטים לא סטנדרטיים (זוהר, מסכות, גלים) אנו כותבים shaders מותאמים אישית ב-GLSL. הם רצים על ה-GPU, חוסכים במשאבי CPU ומגבירים FPS. ה-shaders משולבים דרך Three.js ShaderMaterial או WebGL טבעי.למה Three.js הוא הסטנדרט לאנימציות תלת-ממד באתר?
לסצנות תלת-ממד מורכבות ברקע האתר, אנו משתמשים ב-Three.js — הוא מספק shaders מוכנים, פוסט-פרוסינג ותאימות ל-WebGL 2.0. אנו כותבים חומרים ואנימציות מותאמים אישית, ושולטים בזיכרון דרך // hooks/useCanvas.ts import { useEffect, useRef } from 'react' import { CanvasEngine, RenderFn } from '../lib/canvas-engine' export function useCanvas(renderFn: RenderFn) { const canvasRef = useRef<HTMLCanvasElement>(null) const engineRef = useRef<CanvasEngine | null>(null) useEffect(() => { const canvas = canvasRef.current if (!canvas) return const engine = new CanvasEngine(canvas, renderFn) engineRef.current = engine engine.start() const handleResize = () => engine.resize() window.addEventListener('resize', handleResize) return () => { engine.stop() window.removeEventListener('resize', handleResize) } }, [renderFn]) return canvasRef } .
// components/ThreeBackground.tsx
'use client'
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
export function ThreeBackground() {
const mountRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const mount = mountRef.current!
const width = mount.clientWidth
const height = mount.clientHeight
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.z = 50
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
mount.appendChild(renderer.domElement)
const count = 3000
const positions = new Float32Array(count * 3)
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 200
}
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
const material = new THREE.PointsMaterial({
size: 0.3,
color: 0x3b82f6,
transparent: true,
opacity: 0.7,
})
const points = new THREE.Points(geometry, material)
scene.add(points)
let rafId: number
const animate = () => {
rafId = requestAnimationFrame(animate)
points.rotation.x += 0.0003
points.rotation.y += 0.0005
renderer.render(scene, camera)
}
animate()
const handleResize = () => {
const w = mount.clientWidth
const h = mount.clientHeight
camera.aspect = w / h
camera.updateProjectionMatrix()
renderer.setSize(w, h)
}
window.addEventListener('resize', handleResize)
return () => {
cancelAnimationFrame(rafId)
window.removeEventListener('resize', handleResize)
renderer.dispose()
mount.removeChild(renderer.domElement)
}
}, [])
return <div ref={mountRef} className="absolute inset-0 -z-10" />
} איך אנימציות Canvas משפיעות על Core Web Vitals?
אנימציית Canvas מעוצבת כראוי אינה פוגעת ב-LCP, CLS או INP. אנו משתמשים ב-// lib/particle-system.ts interface Particle { x: number y: number vx: number vy: number radius: number color: string life: number maxLife: number } export class ParticleSystem { private particles: Particle[] = [] private readonly maxParticles: number constructor(maxParticles = 500) { this.maxParticles = maxParticles } emit(x: number, y: number, count = 5) { for (let i = 0; i < count; i++) { if (this.particles.length >= this.maxParticles) break const angle = Math.random() * Math.PI * 2 const speed = 50 + Math.random() * 150 this.particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 100, radius: 2 + Math.random() * 4, color: `hsl(${200 + Math.random() * 60}, 80%, 60%)`, life: 1, maxLife: 0.8 + Math.random() * 0.8, }) } } update(dt: number) { const gravity = 300 this.particles = this.particles.filter(p => { p.x += p.vx * dt p.y += p.vy * dt p.vy += gravity * dt p.vx *= 0.99 p.life -= dt / p.maxLife return p.life > 0 }) } draw(ctx: CanvasRenderingContext2D) { for (const p of this.particles) { ctx.save() ctx.globalAlpha = p.life * p.life ctx.fillStyle = p.color ctx.beginPath() ctx.arc(p.x, p.y, p.radius * p.life, 0, Math.PI * 2) ctx.fill() ctx.restore() } } } וב-Suspense לטעינה עצלה, כך שהאנימציה מתחילה רק אחרי שהעמוד הופך לאינטראקטיבי, מבלי להאט את הרינדור הראשון.
תהליך העבודה
- ניתוח — אנו לומדים את העיצוב, דרישות הביצועים ומכשירי היעד.
- אב-טיפוס — יצירת MVP של האנימציה עם פרמטרים בסיסיים.
- יישום — כתיבת המנוע עם תבניות Repository, אינטגרציה של React bindings.
- אופטימיזציה — הפחתת קריאות ציור, הוספת LOD, בדיקת Core Web Vitals.
- פריסה — הגדרת CI/CD, בדיקות על מכשירים אמיתיים.
לוחות זמנים משוערים
| סוג אנימציה | לוח זמנים |
|---|---|
| חלקיקים / גלים פשוטים | 1–2 ימים |
| מערכת אינטראקטיבית (פיזיקה) | 3–5 ימים |
| סצנת תלת-ממד עם shaders | 1–2 שבועות |
| פרויקט מורכב (אנימציה + לוגיקה עסקית) | 2–3 שבועות |
מה כלול
- קוד מקור (קומפוננטות TS/React, תיעוד JSDoc)
- אופטימיזציה ל-60 FPS על מכשירי היעד
- התאמה למובייל ול-Retina
- אינטגרציה עם האפליקציה שלך (Next.js, Nuxt, CRA)
- תמיכה טכנית למשך שבועיים לאחר המסירה
מומחים מוסמכים ב-Three.js ו-WebGL. אחריות ביצועים של 60 FPS. הזמינו פיתוח אנימציות Canvas בצוות שלנו וקבלו 60 FPS יציב על כל מכשיר.







