Post category: Articles
Noah Grant • September 21, 2016
In the first post in this series, we looked at how coding within the React paradigm could lead to poor browser performance. We fixed a couple of those issues in the context of an auto-height, slide-down animation, but lets look at the timeline we finished with:
As you can see, were busting a lot of frames with some heavy style recalculations and layouts, and were definitely not hitting 60fps. The reason for this is that were still animating the height property, and the height property changes the position of all of its neighbors. This means that in every frame, the browser has to recalculate styles and positioning for all surrounding elements (and, in reality, every element on the pageuntil we have CSS containment). Hence were not making 60fpsour work per frame is just too much. You can see an example of the janky animation in this Codepen:
See the Pen Slidable, no-FLIP, React by Noah Grant (@noahgrant) on CodePen.
But how do we make a slide-down animation without changing height?We FLIPit!
FLIP stands for First, Last, Invert, and Play, and it is a technique for making animations using CSS transforms instead. Transforms, which run through the compositor, dont affect style or flow properties of any neighboring elements, drastically reducing the amount of work the browser has to do per frame. Heres how we use transforms to our advantage with FLIP:
In the process, we immediately render the accordion body in its entirety and bump everything below it into its own layer to be handled by the GPU. So it kinda looks like this:
For more on FLIP, read it straight from its creator, Paul Lewis: https://aerotwist.com/blog/flip-your-animations.
Before looking at how to FLIP a slide-down animation such as the one in the CodePen, one thing to note is that, unlike the height animation, we need to translate more than just our slidable componentsince transforms happen outside the flow of the document (which is why we dont need to perform all those layouts), we also need to translate everything below the slidable. For example, say we had this DOM structure:
<section> <div> <div class="slidable"> <div class="this-is-the-thing-to-slide-up-and-down"> <p>some content</p> </div> </div> <div class="a-sibling"></div> <div class="another-sibling"></div> </div> <div class="bottom-part-of-the-section"> <!-- --> </div> </section>
We would need to translate not just .this-is-the-thing-to-slid-up-and-down
, but also .a-sibling
, .another-sibling
, and .bottom-part-of-the-section
. In React, this means manipulating elements from within the Slidable
component that are not within the Slidable
s component tree. Normally, React would control any style transformations, but controlling styles outside of a components tree is not something its well-equipped to do. So, to make this work, we once again have to go outside of the React paradigm.
Because Browser DGAF that you use React.
So, taking the Slidable
we ended with in the previous blog post, we literally pass in the selector string for all DOM elements we want to translate as a prop:
<section> <div> <Slidable FLIPSelector='.a-sibling, .another-sibling, .bottom-part-of-the-section' updateTriggerCondition={this.state.myCondition} > {this.state.myCondition ? ( <div className='this-is-the-thing-to-slide-up-and-down'> <p>some content</p> </div> ) : null} </Slidable> <div className='a-sibling'></div> <div className='another-sibling'></div> </div> <div className='bottom-part-of-the-section'> <!-- --> </div> </section>
Our Slidable
s componentDidUpdate
and onTransitionEnd
now look like this:
// ... componentDidUpdate(prevProps) { var containerEl = React.findDOMNode(this.refs.container), contentHeight, translateHeight, FLIPEls; if (this.props.updateTriggerCondition !== prevProps.updateTriggerCondition) { // this.state.prevHeight is First. heres our Last contentHeight = React.findDOMNode(this.refs.content).offsetHeight; // this is how much we are going to Invert by translateHeight = this.state.prevHeight - contentHeight; if (contentHeight !== this.state.prevHeight && !this.state.transitioning) { this.setState({ height: !this.props.FLIPSelector ? this.state.prevHeight : null, transitioning: true }, () => { if (this.props.FLIPSelector) { FLIPEls = [...document.querySelectorAll(this.props.FLIPSelector)]; FLIPEls.forEach((el) => { el.style.position = 'relative'; el.style.transform = `translateY(${translateHeight}px)`; el.style.willChange = 'transform'; el.style.zIndex = 100000; }); containerEl.style.overflowY = 'visible'; // sometimes the first raf called will be invoked in the same call // stack, which won't trigger an animation. window.requestAnimationFrame(() => { FLIPEls.forEach((el) => { // and this is where we Play! el.style.transform = 'none'; el.style.transition = `transform ${this.props.transitionDuration}ms`; }); window.setTimeout(this.onTransitionEnd, this.props.transitionDuration); }); } else { // by default we just animate height as before window.requestAnimationFrame(() => { window.setTimeout(this.onTransitionEnd, this.props.transitionDuration); this.setState({height: contentHeight}); }); } }); } } } onTransitionEnd() { var childrenNumberToRemove = (this._isPreviousChildrenSecond()) ? 1 : 0, containerEl = React.findDOMNode(this.refs.container); if (this.props.FLIPSelector) { // unset all changed CSS on any of the FLIPped elements [...document.querySelectorAll(this.props.FLIPSelector)].forEach((el) => { el.style.position = ''; el.style.transform = ''; el.style.transition = ''; el.style.willChange = ''; el.style.zIndex = ''; }); containerEl.style.overflowY = ''; } this.setState({ [`children${childrenNumberToRemove}`]: null, height: null }, this.props.onChangeHeight); } // ...
The code isnt very React-y, but the result sure is pretty! Take a look:
See the Pen React FLIP by Noah Grant (@noahgrant) on CodePen.
So smoove! Here’s our new, much-improved timeline:
We can take this even further and implement FLIPping for a list of accordions as shown in the graphic above, like an FAQ section or the API logsin our console. In this case, we want to translate just a select portion of the selectors we pass to <Slidable />.
But Ill leave that as an exercise for the reader.
A HUGE thank you to Paul Lewis for creating and championing this idea! I highly recommend watching his talks on FLIP and RAIL, if for no other reason than to giggle at the way he says schedule (hes British).
Until next time, remember, as always: Browser DGAF.