Some thoughts on deferred reflows

17 views
Skip to first unread message

Ruben Vreeken

unread,
Sep 23, 2015, 2:39:34 PM9/23/15
to Enyo Development
Recently, there have been two commits to enyo with regards to deferring reflows.
I've been working on similar strategies to reduce reflows at my current project and ran into a few issues I'd like to share here. Perhaps they might be of use to the team and enyo in general.

First off, I think it is a great idea to defer reflows for hidden components. It can confirm from experience that hiding a view and deferring reflows can vastly improve performance compared to tearing down and re-rendering or even destroying and re-creating it.

I did notice a few things about the current implmentation though. First off, I think the current implementation entangles concerns that should be dealt with separately.

Originally, the reflow method had one purpose: 
- To execute any code required to ensure the component is displayed properly in the browser. Preferably, this code would only deal with issues that cannot be solved with CSS alone.

Now that the reflow method is able to defer itself though, it has two purposes:
- To determine if a reflow should take place at all.
- To execute the any code required to ensure the component is displayed properly in the browser, assuming it is *always* contained within a layout component.

The responsibilities of the reflow method on layout kinds have changed in a similar fashion.


While this doesn't necessarily need to cause problems, uncareful usage of the new reflow method will.

Given the fact that reflows often require only very small and simple changes to the DOM, I think it's safe to assume many users simply replaced or extended the default reflow method and added layout code directly to the reflow method rather than than implementing a completely separate layout component. I know I do this on occation and I doubt I' m the only one.

Now that the reflow method has two responsibilities though, this strategy becomes problematic. In cases where the reflow method had previously been extended/replaced, the reflow of the component tree could end up getting partially deferred and partially executed immediately.
In this case, a component sub-tree's components are no longer guaranteed to reflow in the correct order. This does not *always* result in a broken layout, but when it does, it can be very difficult to work out exactly what's going wrong and why. Especially when unaware that the reflow is now expected to able to defer itself.


Instead, I would propose the reflow method gets split into two methods:
- One that decides if a reflow should take place or should be deferred.
- And another that executes the reflow, unaware if its execution was ever deferred at all.

If it is required to let a layout component decide whether or not to trigger a reflow éven if it's container is already showing, the layout component's reflow method should similarly be split in two:

/**
 * Determines if a reflow should take place or if it should be deferred instead.
 * Additional logic can be added to the layoutKind if needed.
 *
 * @private
 */

enyo
.Control.shouldReflow: function() {
   
if (this.layout) {
       
this._needsReflow = this.showing ? this.layout.shouldReflow() : true;
   
}
   
return !this._needsReflow;
}

/**
 * Immediately executes any logic required to properly reflow and layout the rendered component.
 * This method can be replaced or extended freely.
 *
 * @public
 */

enyo
.Control.reflow: function() {

   
if (this.layout) {
       
this.layout.reflow();
   
}
}

// Example immediate reflow
if (this.shouldReflow()) {
    this.reflow();
    this._needsReflow = false;
}

// Example deferred reflow
if (this._needsReflow) {
    this.reflow();
}


Second, I ran into an issue with a plain unextended enyo.DataRepeater. In my current application, there appears to be a scenario where the repeater can destroy one of it's child components and trigger a reflow on it *after* the child's node has been removed from the DOM, but *before* the child has been marked as destroyed. I have been unable to track down the exact cause as it turned out to be exceedingly complicated to debug and it's solution was much easy to work out without knowing the exact cause. Anyways, I ended up adding the following
code to my application:

enyo.Control.extend({
    destroy: enyo.inherit(function(sup) {
        this._needsReflow = false;
        sup.apply(this, arguments);
    });
});

I'm unsure whether this problem is specific to my application or inherent to the way enyo.DataRepeater works, but it might be worth exploring if the problem occurs with any of the various repeater implementations currently available in v2.6*.

Ruben Vreeken

unread,
Sep 23, 2015, 3:01:32 PM9/23/15
to Enyo Development
It would also be cool if reflows could be deferred based on the absoluteShowing property of a component, rather than it's showing property.
Just as it makes little sense to reflow a hidden component, it also makes little sense to reflow a 'showing' component if it has a hidden ancestor.

It's currently an expensive operation to work out whether a control is absoluteShowing. This is mostly due to two things:
- The fact that any controls' absoluteShowing property is left undefined at creation time, leaving the initial absoluteShowing state of the entire component tree undetermined.
- The fact that onShowingChanged events are not propagated when a hidden component is encountered.

I've tried to work out an alternate implementation that attempts to figure out a component's initial absoluteShowing state as soon as it's showingState is determined (at creation time) or whenever it's parent is changed.
The onShowingChanged event is then allowed to propagate the absoluteShowing state. Propagation is only stopped if the receiving component's showing state does not match the incoming absoluteShowing state. If a discrepancy is detected a new, accurate onShowingChanged will be created and propagated instead.

My current implementation is based on enyo 2.4, but should largely - if not entirely - apply for enyo 2.6 as well:

/**
 * Patch enyo.Control to provide better and more accurate tracking of
 * `absoluteShowing` state.
 */

enyo
.Control.extend({


   
/**
     * If we have a new parent, we need to send showingChanged events to our
     * child components.
     *
     * @private
     */

    parentChanged
: enyo.inherit(function(sup) {
       
return function(oldParent) {
            sup
.apply(this, arguments);
           
this.sendShowingChangedEvent();
       
};
   
}),


   
/**
     * Upgrades the onShowingChanged event such that it broadcasts the
     * whether the component is actually visible.
     *
     * If no previous showing value was given, we assume a full visibility
     * check for the parent is required by determining if the parent is
     * absoluteShowing.
     *
     * @private
     */

    sendShowingChangedEvent
: function(wasShowing) {
       
var wasAbsoluteShowing = this.get("AbsoluteShowing"),
            parent
= this.parent,
            isShowing
= this.get("showing"),
            parentShowing
,
            isAbsoluteShowing
;


       
// Determine if parent is absoluteShowing
       
if(parent) {
            parentShowing
= parent.get("absoluteShowing");
           
// Parent's absoluteShowing property not yet determined.
           
if(parentShowing !== true && parentShowing !== false) {
                parentShowing
= parent.getAbsoluteShowing(true);
           
}
       
}
       
// No parent means no parent to hide us.
       
else {
            parentShowing
= true;
       
}


       
// Determine accurate absoluteShowing value
        isAbsoluteShowing
= this.generated && !this.destroyed && parentShowing && isShowing;


       
// Waterfall the real visibility
       
if(isAbsoluteShowing !== wasAbsoluteShowing) {
           
this.waterfall("onShowingChanged", {
                originator
: this,
                showing
: !!isAbsoluteShowing
           
});
       
}
   
},


   
/**
     * Upgrades [showingChangedHandler]{@link enyo.Control.showingChangedHandler}
     * such that it stores the real visibility value of the control.
     *
     * If the new control itself is not visible, the incoming event won't be
     * propagated and a new accurate event will be generated and waterfalled
     * instead.
     *
     * @private
     */

    showingChangedHandler
: function(inSender, inEvent) {
       
// Incoming visibility does not match this controls' visibility
       
if (inEvent.showing && !this.getShowing()) {
           
// Create new accurate event
           
this.sendShowingChangedEvent();


           
// Don't propagate the wrong visibility value.
           
return true;
       
}
       
// Incoming visibility value appears to be correct, set an
       
// absoluteShowing value.
       
else {
           
// Update absoluteShowing
           
this.set("absoluteShowing", inEvent.showing);


           
// Proceed waterfalling unchanged
           
return false;
       
}
   
},


   
/**
     * Execute deferred reflow if needed.
     *
     * @private
     */

    absoluteShowingChanged
: enyo.inherit(function(sup) {
       
return function() {
            sup
.apply(this, arguments);
           
if (this.get("absoluteShowing") && this._needsReflow) {
               
this._reflow();
           
}
       
};
   
}),


   
/**
     * Override to re-evaluate absoluteShowing when the node has rendered.
     * Calls deferred reflow method.
     *
     * @private
     */

    rendered
: function() {
       
this.sendShowingChangedEvent();
       
this._reflow();
       
for (var i = 0, c;
           
(c = this.children[i]); i++) {
           
if (c.generated) {
                c
.rendered();
           
}
       
}
   
},


   
/**
     * Override to call deferred reflow method.
     *
     * @private
     */

    fitChanged
: function() {
       
this.parent._reflow();
   
},


   
/**
     * Override to call deferred reflow method.
     *
     * @private
     */

    resizeHandler
: function() {
       
this._reflow();
   
},


   
/**
     * Only reflow if absoluteShowing
     *
     * @private
     */

    _reflow
: function() {
       
if (this.get("absoluteShowing")) {
           
this.reflow();
           
this._needsReflow = false;
       
} else {
           
this._needsReflow = true;
       
}
   
},


   
/**
     * Re-evaluate absoluteShowing.
     *
     * @private
     */

    destroy
: enyo.inherit(function(sup) {
       
return function() {

           
this._needsReflow = false;
            sup
.apply(this, arguments);

           
this.sendShowingChangedEvent();
       
};
   
})


});

Ruben Vreeken

unread,
Sep 23, 2015, 3:11:59 PM9/23/15
to Enyo Development
Finally, there are many more things that can be deferred effectively once an accurate absoluteShowing property can be guaranteed such as animations, for instance. 

But it doesn't need to stop there. enyo.Control could potentially be extended to track whether or not it's node is attached to the DOM at all. This could allow detaching a subtree from the DOM and re-attaching it later. I'm not sure if that would yield any significant performance benefits, but I think it's worth exploring.

There could eventually also be a sort of "hybernation" mode for controls to silence their events, stop their notifications, prevent reflows, skip animations, defer rendering, etc. Basically, keeping a component subtree in memory but deferring any significant work until it is explicitly taken out of 'hybernation'. This would ofcourse be a complicated undertaking, perhaps more of an enyo 2.7 or 3.0 thing, but it could be cool.

gray.norton

unread,
Sep 23, 2015, 3:16:23 PM9/23/15
to enyo-dev...@googlegroups.com
Thanks, Ruben!

We’ll write up a longer response shortly, but it’s great to hear your thoughts on this — your thinking is very much in line with ours. In fact, we have out of necessity been experimenting with something very much like the “hibernation” scenario you describe, though not (yet) implemented generically for Control.

- G

--
You received this message because you are subscribed to the Google Groups "Enyo Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to enyo-developme...@googlegroups.com.
To post to this group, send email to enyo-dev...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/enyo-development/33aea23d-c923-42c4-ab83-ab46c4845647%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ruben Vreeken

unread,
Sep 23, 2015, 4:09:03 PM9/23/15
to Enyo Development
You're welcome,

It's great to see things line up so well, I'm looking forward to see what awesome stuff you come up with.

--
Ruben


On Wednesday, September 23, 2015 at 9:16:23 PM UTC+2, gray.norton wrote:
Thanks, Ruben!

We’ll write up a longer response shortly, but it’s great to hear your thoughts on this — your thinking is very much in line with ours. In fact, we have out of necessity been experimenting with something very much like the “hibernation” scenario you describe, though not (yet) implemented generically for Control.

- G

From: Ruben Vreeken <ruben....@gmail.com>
Reply-To: "enyo-dev...@googlegroups.com" <enyo-dev...@googlegroups.com>
Date: Wednesday, September 23, 2015 at 12:11 PM
To: "enyo-dev...@googlegroups.com" <enyo-dev...@googlegroups.com>
Subject: [enyo-dev] Re: Some thoughts on deferred reflows

Finally, there are many more things that can be deferred effectively once an accurate absoluteShowing property can be guaranteed such as animations, for instance. 

But it doesn't need to stop there. enyo.Control could potentially be extended to track whether or not it's node is attached to the DOM at all. This could allow detaching a subtree from the DOM and re-attaching it later. I'm not sure if that would yield any significant performance benefits, but I think it's worth exploring.

There could eventually also be a sort of "hybernation" mode for controls to silence their events, stop their notifications, prevent reflows, skip animations, defer rendering, etc. Basically, keeping a component subtree in memory but deferring any significant work until it is explicitly taken out of 'hybernation'. This would ofcourse be a complicated undertaking, perhaps more of an enyo 2.7 or 3.0 thing, but it could be cool.

--
You received this message because you are subscribed to the Google Groups "Enyo Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to enyo-developme...@googlegroups.com.
To post to this group, send email to enyo-de...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages