Bug 310806

Summary: [threaded-animations] REGRESSION: Flickery animations on https://pudding.cool/2026/03/ivf/
Product: WebKit Reporter: Antoine Quint <graouts>
Component: AnimationsAssignee: Antoine Quint <graouts>
Status: RESOLVED FIXED    
Severity: Normal CC: graouts, simon.fraser, webkit-bug-importer
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
Attachments:
Description Flags
Flag disabled (GOOD)
none
Flag enabled (BAD)
none
Frame by frame
none
Reduction
none
Reduction
none
Reduction none

Antoine Quint
Reported 2026-03-26 04:33:11 PDT
Load https://pudding.cool/2026/03/ivf/ in STP 239 and click on "Parent" and notice how the two yellow bubbles that will appear on the right will not animate in as smoothly with the "Threaded Time-based Animations" flag enabled (the default) as it does when it's disabled.
Attachments
Flag disabled (GOOD) (14.32 MB, video/quicktime)
2026-03-26 04:34 PDT, Antoine Quint
no flags
Flag enabled (BAD) (12.04 MB, video/quicktime)
2026-03-26 04:34 PDT, Antoine Quint
no flags
Frame by frame (438.80 KB, video/quicktime)
2026-03-26 04:41 PDT, Antoine Quint
no flags
Reduction (539 bytes, text/html)
2026-04-10 00:52 PDT, Antoine Quint
no flags
Reduction (1.16 KB, text/html)
2026-04-10 01:29 PDT, Antoine Quint
no flags
Reduction (862 bytes, text/html)
2026-04-10 01:41 PDT, Antoine Quint
no flags
Antoine Quint
Comment 1 2026-03-26 04:33:21 PDT
Antoine Quint
Comment 2 2026-03-26 04:34:36 PDT
Created attachment 478804 [details] Flag disabled (GOOD)
Antoine Quint
Comment 3 2026-03-26 04:34:52 PDT
Created attachment 478805 [details] Flag enabled (BAD)
Antoine Quint
Comment 4 2026-03-26 04:36:49 PDT
I meant the purple bubbles (not yellow).
Antoine Quint
Comment 5 2026-03-26 04:41:16 PDT
Created attachment 478806 [details] Frame by frame The issue is that we see a flash of the final frame before the animation runs, as shown in the attached frame-by-frame screen recording.
Antoine Quint
Comment 6 2026-03-26 23:38:04 PDT
Source code for the article is here: https://github.com/the-pudding/ivf.
Antoine Quint
Comment 7 2026-04-09 03:06:58 PDT
The issue goes away if I remove this line (https://github.com/the-pudding/ivf/blob/925c270d44ab148eb3927d4e94e28bc0331a98a8/src/components/Html.svelte#L267): delay: prefersReducedMotion.current ? 0 : DELAY + i * 250 Now I need to figure out what this "fly" directive does under the hood (https://svelte.dev/docs/svelte/svelte-transition#fly) in order to reduce the content to a simple test.
Antoine Quint
Comment 8 2026-04-09 03:45:34 PDT
This fly function is defined in https://github.com/sveltejs/svelte/blob/7be1a0247f1ea84db2e951ab27716c68de5b0650/packages/svelte/src/transition/index.js#L79 and looks like this: const style = getComputedStyle(node); const target_opacity = +style.opacity; const transform = style.transform === 'none' ? '' : style.transform; const od = target_opacity * (1 - opacity); const [x_value, x_unit] = split_css_unit(x); const [y_value, y_unit] = split_css_unit(y); return { delay, duration, easing, css: (t, u) => ` transform: ${transform} translate(${(1 - t) * x_value}${x_unit}, ${(1 - t) * y_value}${y_unit}); opacity: ${target_opacity - od * u}` };
Antoine Quint
Comment 9 2026-04-09 13:14:38 PDT
This is going to be something to do with DOM mutations and transitions happening in quick succession.
Antoine Quint
Comment 10 2026-04-09 13:35:31 PDT
Some very surprising choices made by Svelte here :) First of all, the code above does not yield a CSS Transition but two JS-originated Web Animations. The first one is an animation that sets the base value for the duration of the delay. Then a second animation follows with 60 keyframes specified for each second the animation lasts! I guess this is so they have full control of how values are blended, but it's kind of overkill for a simple transition like the one I'm working with in my Svelte reduction… Haven't quite gotten to the point where I have a fairly simple reproduction, but I am getting closer!
Antoine Quint
Comment 12 2026-04-09 14:28:52 PDT
Haven't quite managed to get it to reproduce as a standalone HTML file without using Svelte that could be used as a layout test, but I have enough to work on a fix.
Antoine Quint
Comment 13 2026-04-10 00:52:34 PDT
Created attachment 478987 [details] Reduction Attached reduction exhibits the problem fairly reliably with STP 240 with Threaded Time-based Animations enabled. Here's the basic code: const div = document.body.appendChild(document.createElement("div")); div.textContent = "Hello!"; const delay = div.animate({ opacity: [0, 0] }, 1500); delay.onfinish = () => div.animate({ opacity: [0, 1] }, { duration: 1000, fill: 'forwards' }); What's happening is that there's a moment where the remote layer tree still has the "delay" animation but the second animation is not started yet and we're flashing the element at opacity=1 because the delay animation has fill="remove".
Antoine Quint
Comment 14 2026-04-10 01:29:54 PDT
Created attachment 478991 [details] Reduction More deterministic reduction forcing the web process to be unresponsive for a while past the natural finish time for the initial animation.
Antoine Quint
Comment 15 2026-04-10 01:41:30 PDT
Created attachment 478993 [details] Reduction Better reduction still with a single animation.
Antoine Quint
Comment 16 2026-04-10 02:14:22 PDT
Here's how we approached this issue with the non-threaded code path in `GraphicsLayerCA::setupAnimation()`: ``` case GraphicsLayerAnimation::FillMode::None: fillMode = PlatformCAAnimation::FillModeType::Forwards; // Use "forwards" rather than "removed" because the style system will remove the animation when it is finished. This avoids a flash. break; ``` So perhaps we just need to make it so that we use a simulated "forwards" fill mode for removed animations in the remote layer tree. However, we must be careful here because this is coming from a world where additivity was not a thing. We must only do this for the cast where an animation is not composing with another animation in the stack.
Antoine Quint
Comment 17 2026-04-10 02:40:26 PDT
So simply forcing the fill to "forwards" for the remote layer tree animation addresses the issue both in the reduced test and on the live site, that's good! But I don't think we can simply do that in all cases where the fill-mode was specified as "none", we need to selectively determine whether that will create issue when animation effects are composed in the remote layer tree. We should be able to analyze this when a `RemoteAnimationStack` is created since we have all the timing information available to us, or perhaps do this on the web process side where much of the required surgery on effects is already performed.
Antoine Quint
Comment 18 2026-04-10 14:23:09 PDT
EWS
Comment 19 2026-04-10 23:43:19 PDT
Committed 310985@main (ebfab2641e50): <https://commits.webkit.org/310985@main> Reviewed commits have been landed. Closing PR #62487 and removing active labels.
Note You need to log in before you can comment on or make changes to this bug.