اولین انیمیشنی که برای یک پروژه‌ی فروشگاهی نوشتم، از نظر خودم عالی بود؛ کارت محصول با یک حرکت نرم به سمت بالا می‌آمد و بعد محو می‌شد. اما وقتی مدیر محصول لینک را روی گوشی خودش باز کرد، واکنشش تکان‌دهنده بود: «این چرا لگ می‌زند؟». در دسکتاپ روان و بی‌نقص بود. در گوشی اندرویدیِ میان‌رده، هر فریم کشیده می‌شد و همان انیمیشن، تجربه را بدتر از نبودش می‌کرد. آن روز یاد گرفتم که انیمیشن در CSS یک تصمیم زیبایی‌شناختی نیست؛ یک تصمیم عملکردی (performance) است که کیفیت اجرا روی ضعیف‌ترین دستگاهی که کاربر دارد، همه‌چیز را درباره‌اش مشخص می‌کند.

انیمیشن در CSS از دو ابزار پایه ساخته می‌شود: transition برای تغییرات نرم بین دو حالت، و @keyframes برای دنباله‌های چندمرحله‌ای. اما این دو ابزار ساده، وقتی روی main thread مرورگر اجرا می‌شوند، تفاوت‌های عمیقی با هم پیدا می‌کنند. اگر در مسیر آموزش CSS از صفر هستید، این نوشته مرحله‌ای است که کد شما را از «کار می‌کند» به «روان کار می‌کند» می‌رساند.

چرا انیمیشن ساده در CSS این‌قدر سخت است؟

نوشتن یک انیمیشن CSS در نگاه اول ساده است: یک خط transition و یک کلاس :hover. اما چالش واقعی وقتی شروع می‌شود که این انیمیشن باید روی سخت‌افزارهای متفاوت، در شرایط مختلف شبکه و با محتوایی که خودتان کنترلی روی آن ندارید، رفتار قابل‌قبولی داشته باشد. سه لایه‌ی مشکلی که در پروژه‌ها دیده‌ام:

  • انتخاب خاصیت اشتباه: انیمیت کردن width، height، margin یا top باعث اجرای مجدد layout در هر فریم می‌شود. در دسکتاپ فرق نمی‌کند، در موبایل تفاوت محسوس است.
  • نداشتن مدل ذهنی درست از زمان‌بندی: انیمیشنی که با easing خطی (linear) اجرا می‌شود، هر چقدر هم که بی‌نقص کدنویسی شده باشد، برای چشم انسان مصنوعی به نظر می‌رسد. تفاوت بین یک انیمیشن حرفه‌ای و یک انیمیشن آماتور، معمولاً در همان منحنی زمان‌بندی است.
  • اجرا روی main thread در زمان اشتباه: اگر انیمیشن هم‌زمان با یک فایل جاوااسکریپت سنگین اجرا شود، مرورگر مجبور است بین آن‌ها تعادل برقرار کند و نتیجه، لگ و پرش است. برای درک این تعامل، بهینه سازی جاوااسکریپت را پیشنهاد می‌کنم.

حقیقتی که در طول پروژه‌ها فهمیدم: یک انیمیشن خوب، انیمیشنی نیست که بیشترین حرکت را داشته باشد؛ انیمیشنی است که کمتر از انتظار کاربر، در مسیر کار او قرار بگیرد.

انیمیشن حرفه‌ای، آن است که وقتی حذفش کنید، کاربر حس می‌کند چیزی کم است، اما در حین اجرا هیچ‌وقت متوجهش نمی‌شود.

Transition: ابزار پایه برای تغییرات نرم

transition ساده‌ترین راه برای نرم کردن تغییر یک مقدار است. بین دو حالت (مثلاً عادی و :hover) قرار می‌گیرد و تغییر را در بازه‌ی زمانی پخش می‌کند:

.button {
  background: #0055ff;
  transition: background 0.2s ease;
}

.button:hover {
  background: #0033aa;
}

سه نکته‌ی ظریف که در پروژه‌ها به‌کارم آمده:

  • همیشه transition را روی حالت پایه بنویسید، نه روی :hover. اگر روی :hover بنویسید، هنگام خروج ماوس انیمیشن وجود نخواهد داشت. این یکی از بیشترین سردرگمی‌های تازه‌کارهاست.
  • انتخاب خاصیت را محدود کنید. transition: all در نگاه اول راحت است اما هر تغییر ناخواسته‌ای را هم انیمیت می‌کند و می‌تواند باگ‌های عجیب بسازد. به‌جای آن، صریحاً بگویید کدام خاصیت انیمیت شود.
  • برای عناصری که مکرر تعامل می‌گیرند، از transition استفاده کنید، نه از animation. transition سبک‌تر است و مدیریت حالت با آن ساده‌تر است.

الگویی که در پروژه‌های اخیر شخصی‌ام استفاده کرده‌ام: تعریف مدت‌زمان‌ها و منحنی‌های استاندارد در متغیرهای CSS تا همه‌ی کامپوننت‌ها همان حس حرکتی را داشته باشند:

:root {
  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
  --duration-fast: 150ms;
  --duration-normal: 250ms;
}

.button {
  transition: background var(--duration-fast) var(--ease-out);
}

Keyframes: انیمیشن‌های چندمرحله‌ای

برای انیمیشن‌های چندمرحله‌ای، از @keyframes استفاده می‌کنیم:

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: fade-up var(--duration-normal) var(--ease-out) both;
}

نکات عملی که در پروژه‌ها روی آن‌ها تأکید کرده‌ام:

  • همیشه از from و to استفاده کنید، نه ۰٪ و ۱۰۰٪، مگر اینکه انیمیشن چند مرحله‌ی میانی داشته باشد. از نظر خوانایی، تفاوت زیادی می‌سازد.
  • animation-fill-mode: both را جدی بگیرید. این مقدار تضمین می‌کند که قبل و بعد از انیمیشن، عنصر روی مقدار اول و آخر بماند. بدون آن، عنصر ممکن است قبل از شروع، حالت اولیه‌ی عادی داشته باشد و یک پرش ناخواسته بسازد.
  • انیمیشن‌های نامحدود (infinite) را با احتیاط استفاده کنید. هر انیمیشنی که نامحدود اجرا می‌شود، تا زمانی که صفحه باز است، منابع مصرف می‌کند. در پروژه‌ای که یک اسپینر نامحدود داشت، مصرف CPU در لپ‌تاپ کاربران به‌طور محسوس بالا رفت. راه‌حل: با IntersectionObserver وقتی اسپینر از دید خارج شد، انیمیشن را متوقف کنید.

Timing Functions و راز انیمیشن‌های طبیعی

تفاوت بین انیمیشن آماتور و حرفه‌ای، تقریباً همیشه در timing function است. جدول زیر نگاه سریعی به مقادیر پرکاربرد می‌دهد:

مقداراحساسکاربرد
linearمکانیکی، رباتیکتقریباً هیچ‌جا در UI، مگر انیمیشن‌های تکراری
easeپیش‌فرض، متعادلشروع خوب، اما کلیشه‌ای
ease-inشروع آرام، پایان سریعخروج عناصر از صفحه
ease-outشروع سریع، پایان آرامورود عناصر؛ حسی طبیعی و انسانی
cubic-bezier(...)کاملاً سفارشیانیمیشن‌های خاص برند

یک قاعده‌ی سرانگشتی که در پروژه‌هایم اثبات شده: برای ورود عناصر، از ease-out یا منحنی سفارشی شبیه آن استفاده کنید. برای خروج، از ease-in. دلیلش ساده است: چشم انسان به حرکت‌هایی که سرعت‌شان کم می‌شود، طبیعی‌تر واکنش می‌دهد. همان چیزی که در فیزیک واقعی هم اتفاق می‌افتد؛ اجسام در حال حرکت، به‌تدریج کند می‌شوند، نه به‌تدریج تند.

اگر بخواهم فقط یک توصیه از تمام این مقاله داشته باشم: ease-out را برای همه‌ی ورود عناصر پیش‌فرض کنید و بعد در موارد خاص سفارشی‌سازی کنید.

transform و opacity: تنها دو خاصیت امن

مهم‌ترین تصمیم در انیمیشن CSS، انتخاب خاصیت انیمیت‌شدنی است. از نظر عملکرد موتور رندر مرورگر، خاصیت‌ها به سه دسته تقسیم می‌شوند:

  1. خاصیت‌هایی که فقط composite می‌شوند: transform و opacity. این‌ها روی GPU اجرا می‌شوند و main thread را درگیر نمی‌کنند. همیشه به‌عنوان اولین گزینه در نظر بگیرید.
  2. خاصیت‌هایی که paint را تحریک می‌کنند: color، background-color، box-shadow. کمی سنگین‌تر از دسته‌ی اول، اما معمولاً قابل قبول.
  3. خاصیت‌هایی که layout را تحریک می‌کنند: width، height، margin، padding، top، left، font-size. این‌ها در هر فریم، کل محاسبات چیدمان را از نو تحریک می‌کنند. در موبایل‌های میان‌رده، خیلی سریع خودشان را نشان می‌دهند.

ترجمه‌ی عملی این دسته‌بندی: به‌جای انیمیت کردن left: 100px، از transform: translateX(100px) استفاده کنید. به‌جای width: 200px، از transform: scaleX(2). این تبدیل‌ها در نگاه اول تفاوت ندارند اما در پروفایلر مرورگر، تفاوت فاحش است.

یک مثال از پروژه‌ای که دقیقاً همین را نشان داد: مودالی که با انیمیت کردن width باز می‌شد، در دسکتاپ روان بود اما در موبایل هر فریم، layout درخت DOM را بازمحاسبه می‌کرد. با تغییر به transform: scale() همراه با opacity، انیمیشن به یک لایه‌ی GPU منتقل شد و مشکل کاملاً حل شد. اگر در پروژه‌ای با مشکل مشابه روبرو هستید، بهینه‌سازی CSS و بهینه‌سازی سرعت سایت را در کنار این بخش بخوانید.

will-change و تله‌ی استفاده‌ی نابجا

خاصیت will-change به مرورگر می‌گوید که برای یک عنصر خاص، تغییرات آتی وجود دارد و بهتر است از قبل لایه‌ی GPU برایش رزرو کند:

.modal {
  will-change: transform;
}

اما این یک شمشیر دولبه است. در پروژه‌ای که با تیم فنی روی یک اپلیکیشن داشبوردی کار می‌کردیم، will-change روی تمام کارت‌ها اعمال شده بود و در نتیجه، مرورگر برای هر کارت یک لایه‌ی GPU مستقل رزرو می‌کرد. نتیجه غیرمنتظره: مصرف حافظه‌ی GPU به‌شدت بالا رفت و در نهایت انیمیشن‌ها کندتر از قبل شدند.

سه اصل عملی برای استفاده از will-change:

  • فقط برای عناصری که واقعاً انیمیت می‌شوند. نه برای همه‌ی کارت‌ها، نه برای همه‌ی دکمه‌ها.
  • در زمان انیمیشن فعال کنید، بعد از پایان حذف کنید. با جاوااسکریپت مقدار را در زمان animationstart ست کنید و در animationend حذف کنید.
  • برای انیمیشن‌های کوتاه، معمولاً لازم نیست. مرورگرهای مدرن امروز آن‌قدر باهوش هستند که برای انیمیشن‌های زیر ۲۰۰ میلی‌ثانیه خودشان تصمیم بهینه بگیرند.

الگوهای واقعی از پروژه‌ها

سه الگویی که بیشترین استفاده را در پروژه‌هایم داشته‌اند:

  1. ورود نرم کارت‌ها: در صفحاتی که محتوا با اسکرول بارگذاری می‌شود، کارت‌ها با ترکیب opacity و translateY وارد می‌شوند. کلید این الگو، محدود بودن برد انیمیشن است: چیزی حدود ۱۰ تا ۱۶ پیکسل کافی است. انیمیشن‌های ورود بزرگ‌تر، به‌جای حس حرفه‌ای، حس شلوغی می‌دهند.
  2. لودر و اسپینر: انیمیشن‌های نامحدود در این دسته قرار می‌گیرند. توصیه‌ی من: از transform: rotate() به‌جای هر چیز دیگر استفاده کنید. این سبک‌ترین راه ممکن است.
  3. ترنزیشن مودال: ورود و خروج مودال، شایع‌ترین انیمیشنی است که در پروژه‌های مدرن اجرا می‌شود. ترکیب transform: scale() و opacity با ease-out برای ورود و ease-in برای خروج، الگوی استانداردی است که به‌ندرت شکست می‌خورد.

برای اینکه این الگوها روی چیدمان‌های پیچیده به‌درستی بنشینند، پیشنهاد می‌کنم ابتدا فلکس باکس در CSS و گرید در CSS را مرور کنید. اگر روی پروژه‌های وردپرسی کار می‌کنید، این انیمیشن‌ها را در قالب چایلد بگذارید تا با آپدیت قالب از دست نروند.

دسترس‌پذیری: احترام به prefers-reduced-motion

یادم می‌آید در یک پروژه‌ی مشاوره‌ای، کاربری با اختلال حرکتی اعلام کرد که انیمیشن‌های سایت باعث سرگیجه‌ی او می‌شود. پس از بررسی، راه‌حل ساده بود: یک media query که به سیستم کاربر احترام بگذارد.

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

این یک الگوی استاندارد است که امروز در تمام پروژه‌های من به‌عنوان قطعه‌ی ثابت وجود دارد. اگر می‌خواهید ببینید دسترس‌پذیری چطور روی همه‌ی اجزای سایت اثر می‌گذارد، استانداردهای دسترس‌پذیری وب و WCAG چیست را بخوانید. توجه به این یک media query ساده، تفاوت بین یک سایت حرفه‌ای و یک سایت بی‌توجه است.

اشتباهاتی که در پروژه‌ها دیدم

  • انیمیت کردن width و height: همچنان شایع‌ترین خطا در پروژه‌های آماتور. هر بار که در پروفایلر مرورگر دیدید layout در حال اجراست، به این خطا شک کنید.
  • استفاده از transition: all: راحت است اما هر تغییر ناخواسته را هم انیمیت می‌کند. در پروژه‌ای که کارتی با این الگو ساخته شده بود، بالا رفتن متن با انیمیشن، باعث انیمیت شدن رنگ متن هم می‌شد و ظاهری عجیب داشت.
  • انیمیشن‌های طولانی: هر انیمیشنی که بیشتر از ۳۰۰ میلی‌ثانیه طول بکشد، در تعامل‌های معمول کاربر حس می‌شود. برای ورود و خروج عناصر، زیر ۲۵۰ میلی‌ثانیه خط قرمز من است.
  • انیمیت بدون احترام به prefers-reduced-motion: مسئله صرفاً زیبایی نیست؛ یک الزام دسترس‌پذیری است.
  • استفاده از left و top برای حرکت: به‌جای transform: translate. دام پرفروش که در هر بازبینی کد پیدا می‌کنم.

برای مطالعه‌ی بیشتر در این دسته، ترفندهای CSS برای طراحی سریع‌تر و انتخابگرهای CSS را در کنار این بخش بخوانید.

تست انیمیشن روی دستگاه واقعی

سه ابزار در پروژه‌هایم برای تست انیمیشن بیشترین استفاده را داشته‌اند:

  1. Performance Tab در Chrome DevTools: با ضبط در حین اجرای انیمیشن، می‌توانید دقیقاً ببینید کدام فریم‌ها کندتر هستند. هر نوار زرد روی main thread، یک فرصت بهینه‌سازی است.
  2. Rendering Tab و Paint Flashing: با فعال کردن این گزینه، مرورگر هر قسمتی از صفحه که دوباره Paint می‌شود را با رنگ سبز نشان می‌دهد. انیمیشن بهینه، در این حالت فقط ناحیه‌ی کوچکی را سبز می‌کند نه کل صفحه را.
  3. گوشی واقعی با اینترنت سلفون: استاندارد طلایی. اگر انیمیشن شما روی گوشی میان‌رده‌ی خودتان روان است، احتمالاً برای ۹۰٪ کاربران هم روان خواهد بود.

برای مطالعه‌ی رویکردی که در پروژه‌های وردپرسی استفاده می‌کنم، افزایش سرعت وردپرس را در کنار این بخش ببینید. اگر سایت روی قالب سنگین اجرا می‌شود، انیمیشن‌ها ممکن است روی همان هزینه‌ی layout موجود سوار شوند؛ در این حالت، مهاجرت به قالب سبک بیشترین اثر را دارد. اگر شک دارید که گلوگاه از قالب است یا از انیمیشن‌ها، دلایل کندی قالب نقطه‌ی تشخیص خوبی است.

زیر پوست ۶۰ فریم: رفتار موتور رندر

اینجا وارد لایه‌ای می‌شوم که در پروژه‌های معمولی دیده نمی‌شود اما برای مهندسان پلتفرم و توسعه‌دهنده‌های ارشد اهمیت دارد. آنچه موتور رندر (Blink در Chrome، WebKit در Safari، Gecko در Firefox) با انیمیشن CSS شما می‌کند، در چهار مفهوم خلاصه می‌شود:

  1. Compositor Thread و Layer Promotion: وقتی یک عنصر را با transform یا opacity انیمیت می‌کنید، مرورگر آن را به یک لایه‌ی compositor ارتقا می‌دهد. این لایه در ریسمانی جدا از main thread پردازش می‌شود و به همین دلیل، انیمیشن حتی در حضور JS سنگین نیز روان باقی می‌ماند. اما هر لایه‌ی اضافه، حافظه‌ی GPU مصرف می‌کند و بالاتر از یک آستانه، خودش به گلوگاه تبدیل می‌شود.
  2. Layout Thrashing در انیمیشن‌های ترکیبی: اگر هم‌زمان با یک انیمیشن transform، جاوااسکریپت شما مکرر مقدار offsetTop یا getBoundingClientRect() را بخواند، مرورگر مجبور می‌شود در هر فریم، layout را از نو محاسبه کند و صف compositor را بی‌اعتبار کند. راه‌حل: خواندن مقادیر layout را در ابتدای هر فریم و جدا از نوشتن‌ها انجام دهید (الگوی read-write batching).
  3. Frame Budget و هزینه‌ی هر فریم: برای انیمیشن روان ۶۰fps، هر فریم ۱۶.۶ میلی‌ثانیه بودجه دارد. از این مقدار، بخشی صرف main thread (JS، style، layout)، بخشی صرف compositor و بخشی صرف paint می‌شود. اگر انیمیشن شما با یک task طولانی جاوااسکریپت هم‌زمان شود، بودجه‌ی main thread تمام می‌شود و در نتیجه هر فریم عقب‌ماندگی می‌گیرد. مطالعه‌ی این تعادل در مفاهیم پیشرفته جاوااسکریپت هم آمده است.
  4. View Transitions API و آینده‌ی انیمیشن وب: API جدید View Transitions که امروز در مرورگرهای مدرن در دسترس است، انیمیشن‌های بین حالات را با یک مدل متفاوت اجرا می‌کند و بسیاری از هزینه‌های render را به سطح مرورگر منتقل می‌کند. برای تیم‌هایی که روی SPA کار می‌کنند، این API می‌تواند جایگزین تمیزتری برای بعضی از انیمیشن‌های جاوااسکریپتی باشد.

یک تجربه‌ی واقعی از پروژه‌ای که با مسئله‌ی Layout Thrashing روبرو شدیم: در یک لیست پیام‌های داینامیک، هر بار که پیام جدیدی اضافه می‌شد، انیمیشن ورود اجرا می‌شد. اما هم‌زمان، اسکریپتی هم ارتفاع لیست را می‌خواند که باعث layout recalculation در هر فریم می‌شد. با تفکیک فاز خواندن از نوشتن و استفاده از ResizeObserver به‌جای خواندن مکرر ارتفاع، مقدار مصرف CPU در همان صفحه به‌طور محسوس کاهش پیدا کرد. اگر روی پروژه‌های وردپرسی هستید، این لایه معمولاً از سمت افزونه‌ها هم تحت تأثیر قرار می‌گیرد؛ برای مشاهده‌ی نقشه‌ی کامل این تعامل، تأثیر افزونه‌ها بر سرعت سایت را ببینید. برای مطالعه‌ی موازی این لایه با CSS و ساختار، استانداردهای HTML و CSS و CSS مدرن از Flexbox تا Grid را در کنار این بخش بخوانید. اگر به تعامل این لایه با تجربه‌ی کاربری و طراحی رابط علاقه‌مندید، اصول طراحی رابط کاربری موفق دید وسیع‌تری می‌دهد.

هر انیمیشنی که در مرورگر روان به نظر می‌رسد، به این معنا نیست که بهینه است؛ فقط یعنی دستگاه شما به اندازه‌ی کافی برای پنهان کردن بدهی فنی سریع است.

فریم پایانی این مسیر

انیمیشن در CSS را می‌توان در یک جمله خلاصه کرد: «ابزاری برای هدایت توجه کاربر، نه برای نمایش قدرت فنی توسعه‌دهنده.» سه درس که در پایان این مسیر روی آن‌ها تأکید می‌کنم:

  1. بهینه‌سازی از انتخاب خاصیت شروع می‌شود. قبل از هر تصمیم دیگری، مطمئن شوید که transform و opacity اولین گزینه‌ی شما هستند. اگر مجبورید از خاصیت‌های دیگر استفاده کنید، دلیلش را با خودتان مکتوب کنید.
  2. timing function را جدی بگیرید. تفاوت بین linear و یک منحنی cubic-bezier خوب، همان تفاوت بین انیمیشن آماتور و حرفه‌ای است. این موضوع، بیش از هر ترفند دیگری روی حس نهایی اثر می‌گذارد.
  3. دسترس‌پذیری بخشی از کار است، نه یک آپشن. یک media query سه‌خطی برای prefers-reduced-motion، همه‌ی پروژه را برای کاربران با اختلال حرکتی قابل استفاده می‌کند. هیچ بهانه‌ای برای نگذاشتنش وجود ندارد.

مسیر یادگیری CSS با انیمیشن تمام نمی‌شود. اگر می‌خواهید مرحله‌ی بعدی را بردارید، متغیرهای CSS و ریسپانسیو با CSS قدم‌های منطقی بعدی هستند. برای درک عمیق‌تر رفتار مرورگر با کد شما، بهینه سازی جاوااسکریپت و بهینه‌سازی سرعت سایت دید وسیع‌تری می‌دهند.

اگر در پروژه‌ای با یک انیمیشن روبرو شده‌اید که در دسکتاپ روان بود اما در موبایل لگ می‌زد و با یک تغییر کوچک (مثلاً جایگزینی left با transform) به‌طور کامل حل شد، جزئیات سناریو را در دیدگاه بنویسید. مخصوصاً اگر با یک مورد غیرمنتظره روبرو شده‌اید — مثلاً انیمیشنی که فقط در گوشی‌های اندروید مشکل داشت — همان تجربه برای خواننده‌ی بعدی از هر توضیح کلی ارزشمندتر است. 🎬