-
Notifications
You must be signed in to change notification settings - Fork 651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Use fastdom to batch writes and avoid sync layout #385
base: master
Are you sure you want to change the base?
Conversation
src/utils/ClassHelpers.js
Outdated
if (process.browser) { | ||
// eslint-disable-next-line global-require | ||
if (!visibilityWatcher) visibilityWatcher = require('visibility')(); | ||
if (visibilityWatcher.visible()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think !document.hidden
is probably fine instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, old habits die hard; updated
src/utils/ClassHelpers.js
Outdated
// eslint-disable-next-line global-require | ||
if (!visibilityWatcher) visibilityWatcher = require('visibility')(); | ||
if (visibilityWatcher.visible()) { | ||
fastdom.mutate(run); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this doing exactly? It feels like overkill if all that's happening is it's its running tasks in a RAF loop no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also batches them into a single transaction (see https://github.com/wilsonpage/fastdom#how-it-works) which can avoid thrashing.
However, if you don't use the visibilityWatcher, a bunch of rAF
calls will batch up and cause a lag while you switch back to the tab, as all those batched up calls execute at once.
// which is necessary in order to transition styles when adding a class name. | ||
if (className) { | ||
/* eslint-disable no-unused-expressions */ | ||
node && node.scrollTop; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the reflow being removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The point is to avoid synchronous layout, which this forces. In my testing it was no longer necessary when using fastdom
as the class manipulation happens on another tick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit concerned about this, bc my previous testing trying to use rAF directly instead of the sync reflow led to animations not running. I wish I had a specific test case to try tho, I don't honestly remember what the cases were. Maybe during interrupts or fast toggling?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't been able to replicate any problems in my testing - ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
theoretically, what is ensuring that active
classes are being added on a different tick than the initial classes? From what I can tell here, every call to fastdom.mutate
adds to the same queue, if the initial and active classes are added within the the rAF time frame (very likely?) they will flush on the same tick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To follow up on this, we've been using this in prod for 3mo+ without any issue. The forced reflow is unnecessary as fastdom will delay the change until the next animation frame.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The -active
classes work as expected, I tested this by adding opacity: 0
to -enter
and opacity: 1
to -enter-active
. The element faded in, which wouldn't happen if they were added in the same tick.
We're using this in prod with no issue. I'll resolve the conflict but it's up to the maintainers to decide if they want this. |
@silvenon i'd appreciate your thoughts here. I think i'm fine with this generally. My only concern is that fastdom is not a widely used package, and for most folks this is going to be just dead weight. I really do like the option tho to have better control over this, handling reflows and transitions across pages with multiple components sucks and this seems like it'd make an improvement. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was manually stress-testing this and I didn't find any regressions. 👍 I didn't test performance because that goes beyond my frontend skills, but I trust that it worked well in your project.
As far as I'm concerned, this works well. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I was linking the package wrong when I was testing it, so I was testing the latest release. 🤦♂️ The -active
classes aren't being added on next tick, so this changes the important part of the behavior.
To try this for yourself, use the following code: <CSSTransition appear in timeout={300} classNames="item">
<div>Item</div>
</CSSTransition> .item-appear {
opacity: 0.01;
}
.item-appear-active {
opacity: 1;
transition: opacity 300ms;
} You'll see that the item doesn't fade in, it just instantly appears. You probably didn't need the appearing functionality, or maybe you were using it in a way which causes a delay which you didn't mind. But adding |
Ok. I'm out for a little while for some family events so I won't be able to get to this immediately but I'll add it to my list to review so I can ensure existing behavior is maintained. Re: fastdom package weight, it's not zero, but it's very small (46kB full package in node_modules, 1.2kB minified, 626B min+gz). |
The "transaction" thing is just... running them all synchronously, no? https://github.com/wilsonpage/fastdom/blob/v1.0.6/fastdom.js#L161-L207 I wonder if we can be smarter and do our own batching given that we have the whole transition group/transition setup. @STRML, in your case, are your transitions all nested under a group component? |
You could do your own batching. For what it's worth, I use fastdom manually in other parts of our codebase so it was very convenient to reuse it here. All fastdom essentially does is batch "reads" and "writes" and flush them synchronously at rAF, reordering the reads before the writes so you don't end up with layout thrashing. For what it's worth, it's very well tested and 600 bytes' savings is not really enough incentive IMO to make it worth reimplementing, considering the test overhead and probability of bugs. |
Sure – it's not about the space savings here. I'm wondering more whether we can do a better job at it, given that we have a semantic concept of transition groups/&c. |
Yeah sure that makes sense. I won't have time to implement it but I'm happy
to test and review.
…On Tue, Oct 9, 2018, 7:19 PM Jimmy Jia ***@***.***> wrote:
Sure – it's not about the space savings here. I'm wondering more whether
we can do a better job at it, given that we have a semantic concept of
transition groups/&c.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#385 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABJFP47s8nHOyf8hdR1r1cY1fH_61D1Bks5ujKXMgaJpZM4Vmw3r>
.
|
Hi - was there something else this PR needed?
…On Mon, Jan 14, 2019, 8:52 AM Jason Quense ***@***.*** wrote:
Closed #385 <#385>.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#385 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABJFP98URKgrsuDoaPjabsDKkZ0ezHL_ks5vDJnEgaJpZM4Vmw3r>
.
|
sorry, I just saw the changed requested and assumed it was stale |
@silvenon if you're concerns are addressed can you review and we can get this moving along |
I hate to be that person but I'm prodding again to see if we can get the ball rolling — would love to see some performance improvements in I've made #496 to fix the enter/appear animations not playing properly and fixing the merge conflict. Explanation on the fix and the results are in the PR. I was about to hijack this PR and push a fix to the branch but I can't since it's in another repo, so I had to make a new PR. Sorry! |
Rebased on master and incorporated @diagramatics fix for #496. Thanks! |
I'm actually still seeing issues in storybook with this, in particular the "animates on appear" story is not working right. If I force a reflow in the |
Okay, I looked at it again and I figured might as well keep the reflow logic in, but batch them too with Here's a patch to the current PR @STRML: diff --git a/src/CSSTransition.js b/src/CSSTransition.js
index 29837a6..91161b7 100644
--- a/src/CSSTransition.js
+++ b/src/CSSTransition.js
@@ -3,7 +3,7 @@ import React from 'react';
import Transition from './Transition';
import { classNamesShape } from './utils/PropTypes';
-import { addClass, removeClass } from './utils/ClassHelpers';
+import { addClass, removeClass, triggerReflow } from './utils/ClassHelpers';
/**
* A transition component inspired by the excellent
@@ -85,6 +85,7 @@ class CSSTransition extends React.Component {
appearing ? 'appear' : 'enter'
);
+ triggerReflow(node)
addClass(node, activeClassName)
if (this.props.onEntering) {
@@ -122,6 +123,7 @@ class CSSTransition extends React.Component {
onExiting = (node) => {
const { activeClassName } = this.getClassNames('exit')
+ triggerReflow(node)
addClass(node, activeClassName)
if (this.props.onExiting) {
diff --git a/src/utils/ClassHelpers.js b/src/utils/ClassHelpers.js
index c039b2d..4e0dcf9 100644
--- a/src/utils/ClassHelpers.js
+++ b/src/utils/ClassHelpers.js
@@ -9,6 +9,10 @@ export function removeClass(node, classes, immediate) {
mutateClass(node, classes, immediate, removeOneClass);
}
+export function triggerReflow(node) {
+ fastdom.measure(() => node.scrollTop);
+}
+
function mutateClass(node, classes, immediate, fn) {
if (!node) return;
if (classes && typeof classes === 'string') { |
WIthout this patch, try the storybook; you'll see a class of 'undefined' being added to elements on mount because the 'appearClassName' and 'enterClassName' are undefined.
Hey @diagramatics. Unfortunately that doesn't actually do it, as you can see in the storybook. Unfortunately I don't think we can actually 100% get what we want, to either batch all reflows (reads) in a single operation or all className edits (writes) in a single operation, without a significant refactor of this module. For example, on an entering widget, we would have to set up operations in this order:
This is a write, a read, then a write, and if we put this through fastdom it's going to reorder it and the animation won't show properly. I can see a possible optimization where we define batch types (appear/enter, reflow, exit/leave), and process those in batches. This would require maintaining a state machine per element to determine which operation it needs, then schedule reads and writes as necessary. I don't know if there is enough benefit to doing this versus this PR, which at least avoids doing a forced synchronous reflow by scheduling on rAF, and does 50% fewer DOM reads by only reflowing on the initial class add. |
Sorry, not sure I'm following where the issue lies. I think I'm missing some extra info. Let me try and understand your comments one by one.
Which story in particular? I tested all of them (with my change and also with your change) and both work fine in the
Do you have an example of this in action? I can't think of any that would add an exit class, then an enter class. I always thought exiting and entering actions happen separately — if you want to exit, then
I thought we can leverate I feel like your transition uses are way more cooler than mine, which is just a simple fade in/out on multiple images at once 😅 and I think that's why I don't fully grasp the issue you have. My issue is around the fact that when you try and do that, it doesn't batch the force reflows as #496 screenshots highlight. I think triggering reflows before the |
src/utils/ClassHelpers.js
Outdated
// need to be done on every single class change, only when the 'active' class is added. | ||
if (classes.indexOf('active') !== -1) { | ||
forceReflow(node); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually against doing this here since it makes the class helpers seem very specific to that active
prefix being there. I think you can change the prefix or set custom CSS classes to apply for?
Doing it in onEntering
and onExiting
just like the previous reflowAndAddClass
function usage seems like the safer option to me. Plus since it's the only place where the active
classes are going to be added I don't think we're going to miss anything. Plus it's safer when the logic flow is pretty much similar, so we don't end up causing breaking changes to anyone else.
Also in addition, why not wrap it in a fastdom.measure
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right about the active
bit, I've fixed in the latest commit.
In my testing it's only needed on onEntering
.
Using fastdom.measure()
does the reflow in a different tick and doesn't show the animation.
With your patch, I don't see either enter or appear animations working in Storybook's "Css Transition Group -> Animates on all" or "Animates on enter" or "Animates on appear". Regarding the reflow bit, I'm not explaining it well. The simplest explanation is that you need a reflow between adding "enter" and adding "enter-active", same with "appear" and "appear-active". Without it, the animation won't trigger. |
Sounds about right! I thought my change covers that reflow by queueing it in Odd that my patch doesn't work on your end. Could it be browser? I haven't tested it in other places other than Chrome, I have to admit 😅 |
I'm using Chrome to test it as well, Chrome 74. However if you put the reflow in |
That's very weird. I thought since That's when I test the Otherwise I'm generally happy with how you've done it because that's what I thought |
its a super thorny area folks :/ that really steams from the core problem that css transitions really aren't meant to be triggered programmatically in this way. The reflow stuff vaguely defies logic, with requestAnimationFrame's not working and you'd expect and hard sync reflows between frames working great. The other added rub is that trying to do this via React's state model means we are actively dealing with two schedulers for updates and react can "fall behind" or flush at seemingly odd moments some times. as with a lot of platform primitives, when you need to do any serious animation work in the context of an app, i tend to think you almost always need to use a js framework like velocity or GSAP (both of which work nicely on top of Transition btw). Apart from that it always feels like this bespoke cat wrangling of animations. That said i'm Really not an expert and if ya'll are happy to dig in or propose even drastic refactors i'm more than open to it |
I'm happy to leave this PR to only cover batching that |
Following up on this @STRML to see if your timeline looks any different to the one I have in #385 (comment) for the |
We avoid the second reflow in this patch (reflow is only strictly necessary Notice this takes about 8ms, because the While thinking about this, I had the idea to actually push all the nodes needing layout into an array so we ensure that we finish all reads (reflows) before writes, rather than thrash back and forth. The results are encouraging and have no loss in functionality: This reduces the actual JS-active time from 8ms to about 3ms. If you count the subsequent "Recalculate Style" (which is done synchronously in the first screenshot, rather than in a batch), the total time is about 5ms, a roughly 37% reduction. |
…changes Prevents unnecessary context switching for a nearly 40% perf improvement when using "appear" on 50 nodes
This is ready for re-test and eventual merge. |
@taion you have thoughts here? |
I don't really have a view here. I'm quite unfamiliar with fastdom and with what makes DOM manipulations efficient. |
When animating a large number of elements, it can be helpful to batch these animations up into a single mutate operation using
fastdom
to improve forced synchronous layout stalls.