LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #18: Laggy Animations – When the touch is no longer smooth
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

Blog #18: Laggy Animations – When the touch is no longer smooth

Why is CSS correct, JS smooth, yet effects still stutter? Exploring the contention between the Main Thread and the Compositor Thread in the browser system.

TP
Truong PhamSoftware Engineer
PublishedApril 28, 2024

"Every time I click to open the Sidebar, this app seems to hitch for a beat before appearing. It feels old and cheap somehow."

This comment from a client made me rethink my whole approach to effects. We often think that a janky animation (low FPS) is because our CSS code isn't optimized. But in a modern Frontend system, animation is the final victim of System Overload.

1. Overall Architecture: The battle between two Threads

In the browser, there is a critical boundary every dev needs to understand: the Main Thread and the Compositor Thread.

  • Main Thread: Where everything happens—from JS execution, Layout calculation, to handling user events. It's like a busy office where only one employee does everything.
  • Compositor Thread: Specialized for drawing layers onto the screen. It runs independently and is extremely fast.

When you run an animation changing width or margin, you are forcing the Main Thread to recalculate the Layout at every frame (60 times/second). If at that exact time the system is performing a large JSON.parse or re-rendering a data table, the Main Thread will be choked. Your animation misses frames (Jank).

2. Analysis of Execution Flow

  • Execution Flow: Network Response → JS Parsing → Main Thread Busy → Style Calc → Layout (Block!) → Paint → Composite.
  • The Issue: If a JS task occupies more than 16ms, you lose a frame. The user will see a "stutter."
/* Common error: "Heavy" animation for the system */
.sidebar-open {
  width: 300px; /* Causes Layout/Reflow on the Main Thread */
  transition: width 0.3s;
}

/* System Approach: Switching to GPU */
.sidebar-open-smooth {
  transform: translateX(0); /* Runs only on the Compositor Thread */
  transition: transform 0.3s;
}

3. Quick Fix vs. Architectural Refactoring

Quick Fix: Use requestAnimationFrame to try and coordinate JS. This is a temporary painkiller. It doesn't solve the fact that the Main Thread is still carrying too much work.

Architectural Refactoring (Architectural Fix):

  1. Offloading: Move heavy calculation tasks off the Main Thread using Web Workers.
  2. Layer Promotion: Use the will-change property to promote the component to its own layer on the GPU, helping it be "immune" to Main Thread fluctuations.
  3. Decoupling: Separate the data processing flow and the display flow. Animation should not depend on the Loading state of the data.

4. Technical Debt and Self-Reflection

Ignoring the difference between Threads is a major technical debt. It creates applications that are "only smooth on powerful computers." As the project grows, these janky animation errors will become extremely hard to trace because they don't lie in the CSS file, but in the overlap of JS tasks across the whole system.

I wonder: "Am I making the system so complex that it's choking the user experience?". Why should a Sidebar effect have to wait for an Analytics logic or an image processing task somewhere deep in the system? Perhaps our encapsulation isn't good enough to protect the most important display flow.

Lesson Learned

Smooth animation isn't the result of a magic line of code. It's the result of a system with Thread Discipline. Respect the 16ms of each frame by freeing the Main Thread from unnecessary work.


Notes on the freedom of the display flow.

Series • Part 18 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #19: Chrome Performance Tab – Looking through a microscope at the system's pulse
Blog #17: Images vs JS – The battle for priority in the Critical Path
13Blog #13: When optimization becomes a burden – Don't 'memo' everything you see14Blog #14: The Domino Effect – When one small change crashes the whole Render system15Blog #15: The 10,000-Row Lesson – When Table Virtualization saved my career16Blog #16: Misplaced Debounce – When a localized solution becomes a system burden17Blog #17: Images vs JS – The battle for priority in the Critical Path18Blog #18: Laggy Animations – When the touch is no longer smoothReading19Blog #19: Chrome Performance Tab – Looking through a microscope at the system's pulse20Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?21Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument22Blog #22: Click Race Condition – When User's click speed beats your App23Blog #23: Multi-tab Syncing – When your application 'talks' between tabs
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles