Build performant UI components

By fully utilizing the GPU

Scrolling


So simple,
and yet,

So complicated...

Scrolling with CSS

  overflow: auto; /*make the surface scrollable*/
  -webkit-overflow-scrolling: touch; /*momentum iOS*/
DeviceSnap viewportBounce type
DesktopfalseNone
Lumia920 (WP8)trueExponential decay
iPhone (iOS7)trueExponential decay
Android 4.1 (chrome)falseNone
Android 4.4 (chrome)falseNone
Blackberry Z10falseNone
  • Pretty row 1
  • Pretty row 2
  • Pretty row 3
  • Pretty row 4
  • Pretty row 5
  • Pretty row 6
  • Pretty row 7
  • Pretty row 8
  • Pretty row 9
  • Pretty row 10

Web Native Scrolling issues

  • No control during scrolling animation
    • Event "scroll" is only fired once the scroll is over
  • No room for customization
    • pullToRefresh, snap, ...
  • Inconsistencies across platforms
    • Bounce effects are different
    • PreventingDefault touch events will break scrolling
    • Nested scrollers behave oddly
    • Applying overflow property disables VoiceOver pagination on iOS
  • Big scrolling areas can be hard to manage for the browser
    • DOM manipulation will cause costly relayouts/paint

Building a synthetic JS scroller

Building JS Scroller (I)

Using CSS transitions


  • Gesture start: Initialize a gesture
  • Gesture move: GPU translate3d DOM node
  • Gesture end: Get velocity
  • Calculate momentum given velocity
    • destination
    • duration
  • Apply CSS transition
    • Apply CubicBezier curve
  • Check valid position
    • If out of boundaries, reset position
  • Transition End: Clean transitions
  • Pretty row 1
  • Pretty row 2
  • Pretty row 3
  • Pretty row 4
  • Pretty row 5
  • Pretty row 6
  • Pretty row 7
  • Pretty row 8
  • Pretty row 9
  • Pretty row 10
  • Pretty row 11
  • Pretty row 13
  • Pretty row 14
  • Pretty row 15
  • Pretty row 16
  • Pretty row 17
  • Pretty row 18
  • Pretty row 19
  • Pretty row 20
  • Pretty row 21
  • Pretty row 22
  • Pretty row 23
  • Pretty row 24
  • Pretty row 25
  • Pretty row 26
  • Pretty row 27
  • Pretty row 28
  • Pretty row 29
  • Pretty row 30

Web Native Scrolling issues

  • No control during scrolling animation
    • Event "scroll" is only fired once the scroll is over
  • No room for customization
    • pullToRefresh, snap, ...
  • Inconsistencies across platforms
    • Bounce effects are different
    • PreventingDefault touch events will break scrolling
    • Nested scrollers behave oddly
    • Applying overflow property disables VoiceOver pagination on iOS
  • Big scrolling areas can be hard to manage for the browser
    • DOM manipulation will cause costly relayouts/paint

Behind every animation...

Calculating velocity

Calculating velocity

Exponential moving average:

function moveStep () { // triggered every 17ms~
    var v = (currentPos - prevPos) / (currentTime - prevTime); // current velocity
    self.velocity = 0.6 * v + 0.4 * self.velocity; // Applying moving average formmula
    self.translate(self.x, self.y); // debounce move event from animation
    requestAnimationFrame(moveStep);
}

Velocity:

  • Pretty row 1
  • Pretty row 2
  • Pretty row 3
  • Pretty row 4
  • Pretty row 5
  • Pretty row 6
  • Pretty row 7
  • Pretty row 8
  • Pretty row 9
  • Pretty row 10
  • Pretty row 11
  • Pretty row 13
  • Pretty row 14
  • Pretty row 15
  • Pretty row 16
  • Pretty row 17
  • Pretty row 18
  • Pretty row 19
  • Pretty row 20
  • Pretty row 21
  • Pretty row 22
  • Pretty row 23
  • Pretty row 24
  • Pretty row 25
  • Pretty row 26
  • Pretty row 27
  • Pretty row 28
  • Pretty row 29
  • Pretty row 30

Calculating easing function

Calculating easing function

Calculating easing function





var curve    = CubicBezier(0.15, 0.72, 0.33, 0.94),
    position = 250,
    duration = 5000;

scroller.scrollTo(position, duration, curve);

Calculating easing function: cubic-bezier


// scroll to a given position in a give time (momentum)
scrollTo: function (pos, duration, curve) {
    var startTime = NOW(),
        deltaPos    = startPos - pos,
        destTime    = startTime + duration;

    function step () {
      var now = NOW();
      if (now >= destTime) { 
          return; // Animation is done!
      }
      percentageTime     = (now - startTime) / duration;
      percentageProgress = curve(percentageTime); 

      // Calculate new position based on the result of the function
      newPos = deltaPos * percentageProgress + startPos;

      requestAnimationFrame(step);
    }
    step();
}
  • Pretty row 1
  • Pretty row 2
  • Pretty row 3
  • Pretty row 4
  • Pretty row 5
  • Pretty row 6
  • Pretty row 7
  • Pretty row 8
  • Pretty row 9
  • Pretty row 10
  • Pretty row 11
  • Pretty row 13
  • Pretty row 14
  • Pretty row 15
  • Pretty row 16
  • Pretty row 17
  • Pretty row 18
  • Pretty row 19
  • Pretty row 20
  • Pretty row 21
  • Pretty row 22
  • Pretty row 23
  • Pretty row 24
  • Pretty row 25
  • Pretty row 26
  • Pretty row 27
  • Pretty row 28
  • Pretty row 29
  • Pretty row 30

Velocity:

Web Native Scrolling issues

  • No control during scrolling animation
    • Event "scroll" is only fired once the scroll is over
  • No room for customization
    • pullToRefresh, snap, ...
  • Inconsistencies across platforms
    • Bounce effects are different
    • PreventingDefault touch events will break scrolling
    • Nested scrollers behave oddly
    • Applying overflow property disables VoiceOver pagination on iOS
  • Big scrolling areas can be hard to manage for the browser
    • DOM manipulation will cause costly relayouts/paint
33,200 px

Surface Manager

Surfaces

.surface {
    transform: translate3d(0, 0, y);
    backface-visibility: hidden;
}

Surface hot swapping


At each frame the SurfaceManager will decide which operations to execute.

Item 01
Item 02
Item 03
Item 04
Item 05
Item 06
Item 07
Item 08
Item 09
Item 10
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
Item 18
Item 19
Item 20

Surface attachment

attachItemInSurface: function (item, surface, config) {
    var offset = config.offset;

    // Recalculate styles (DOM write)
    surface.dom.appendChild(item.dom);

    // This will force a paint (DOM read)
    height = surface.dom.offsetHeight;

    offsetY = offset - height;
    surface.dom.style[transform] = 
        'translate3d(0, ' + offsetY + ', 0)';
}

Plugins


var myPlugin = {
    init: function () {
        this.on('_update', this._updateMyPlugin);   // Register a listener on update
        this.on('_refresh', this._refreshMyPlugin); // Register a listener on refresh
    },
    _getVelocity: function () { // Overrides the original scroller function
      return 2;
    },
    _refreshMyPlugin: function () { // Adds some internal state
      this.state = 'refreshed';
    },
    _updateMyPlugin: function () { // Do something external to the scroller
        var str = this.opts.myPlugin;
        console.log(str + this.y);
    }
};
//Attach plugins to the scroller
var scroller = new Scroller('#wrapper', {myPlugin: 'YAY!: '});
scroller.plug(myPlugin);
//Add and initialize plugins on bootstrap
var scroller2 = new Scroller('#wrapper', {
    plugins: ['MyPlugin2'] // If is already registered under Scroller.plugins
});

What if...

this was available

Today ;)

Speed must be treated as an essential design feature. Everything we add increases the work the browser has to do to put pixels on the screen
Addy
Osmani

Gracias!

For questions, I will be having office hours...