The last time Hackerfall tried to access this page, it returned a not found error. A cached version of the page is below, or click here to continue anyway

Browser DGAF (that you use React) Pt. 2: FLIPping in React - Sift Science Engineering Blog : Sift Science Engineering Blog

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

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:

  1. F: We calculate the initial position of the element we want to animate
  2. L: We move the element to its final position and measure its position
  3. I: We apply a negative transform equal to the difference between the two, which effectively makes it look like the element hasnt moved at all
  4. P: We remove the negative transform, which, if the element has a CSS transform transition applied, will animate gracefully to its final position

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.

An aside on the slide

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 Slidables 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.

FLIP in the real (React) world

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 Slidables 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:

Conclusion

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.

Continue reading on engineering.siftscience.com