Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fast By Default: Modern loading best practices

Addy Osmani
October 27, 2017

Fast By Default: Modern loading best practices

Optimizing sites to load instantly on mobile is far from trivial. Costly JavaScript can take seconds to process, we often aren't sensitive to users data-plans and browsers don't know what UX-critical resources should load first. Thankfully there's a lot we can do to give our users a MUCH better loading experience. Watch Addy Osmani (and friends) illuminate new loading best practices for diagnosing and making real world sites load instantly today.

Video: https://youtu.be/_srJ7eHS3IM

Addy Osmani

October 27, 2017
Tweet

More Decks by Addy Osmani

Other Decks in Technology

Transcript

  1. LOADING BEST PRACTICES
    Modern
    @addyosmani

    View full-size slide

  2. MOBILE CHANGES
    everything.

    View full-size slide

  3. THE NETWORK
    THERMAL THROTTLING
    PARSING JAVASCRIPT
    THIRD-PARTY CODE
    PARSER BLOCKING PATTERNS
    DISK I/O
    CACHE EVICTION
    IPC JANK
    DIFFERENCES IN L2/L3 CACHING
    RTTS
    IMAGES
    WHAT
    IMPACTS
    LOADING?

    View full-size slide

  4. 2017 Best Practices For Loading
    Compress diligently! (GZip, Brotli)
    Cache effectively (HTTP, Service Workers)
    ⚡ Minify & optimize *.*
    Preresolve DNS for critical origins
    Preload critical resources
    Respect data plans
    Stream HTML responses
    Make fewer HTTP requests
    Have a Font loading strategy
    ✂ Send less JavaScript (code-splitting)
    Lazy-load non-critical resources
    Route-based chunking
    Library sharding
    PRPL pattern
    Tree-shaking (Webpack, RollUp)
    Serve modern browsers ES2015 (babel-preset-env)
    1 Scope hoisting (Webpack)
    Don’t ship DEV code to PROD

    View full-size slide

  5. We want happy users.

    View full-size slide

  6. RESPONSE
    ANIMATION
    IDLE
    LOAD
    RAIL
    Evolving

    View full-size slide

  7. First Paint
    First Meaningful Paint
    Time To Interactive
    User happiness metrics
    First Contentful Paint

    View full-size slide

  8. “Networks, CPUs and disks all
    hate you. On the client, you
    pay for what you send in
    ways you can't easily see”
    - Alex Russell, Chrome

    View full-size slide

  9. JavaScript has a cost.
    Fast = Fast at
    Parse
    Eval
    Download
    On mobile devices

    View full-size slide

  10. 2017 JavaScript Parse Costs
    Average Phone
    ~1MB JavaScript (uncompressed)

    View full-size slide

  11. JavaScript Parse Cost On Mobile - CNN
    ~9s difference to the A11
    With thanks to Pat Meenan

    View full-size slide

  12. PRPL Pattern
    USED BY SITES LIKE
    SUPPORTED BY CLIs

    View full-size slide

  13. Tools with a baseline that is
    fast by default provide the
    best chance of success.

    View full-size slide

  14. Where do mobile sites spend their time loading?
    With thanks to Camillo and Mathias @ V8
    Average
    Housing
    Forbes
    Treebo
    Twitter
    Trivago
    Lancome
    Tech Today
    OLACabs
    Wego
    Konga

    View full-size slide

  15. Caching
    In most cases, when a web page needs a resource, Chrome starts by looking it
    up in the Memory cache. If the Memory cache doesn’t have it, Chrome will then
    ask the network stack to handle the request. The network stack will eventually
    process the request and will start by looking for the resource in the HTTP cache.
    If the HTTP cache doesn’t have it, the network stack will then issue an actual
    network request.

    View full-size slide

  16. * Chrome has 4+ caches. The above reflects the main two - the HTTP and memory caches
    Chrome’s Cache Hit Rates

    View full-size slide

  17. EVERYONE IS
    RESPONSIBLE FOR
    performance.

    View full-size slide

  18. “File-size isn't just about the
    download. Byte-for-byte, JavaScript
    is more expensive for the browser
    to process than the equivalently
    sized image or Web Font.”
    - Tom Dale, Glimmer & Ember

    View full-size slide

  19. https://twitter.com/kristoferbaxter/status/908144931125858304

    View full-size slide

  20. GOOD OPTIONS FOR MOBILE WEB
    POLYMER PREACT VUE.JS
    GLIMMER SVELTE REACT
    *WITH CODE-SPLITTING
    AND A PERF BUDGET
    AND OTHERS, LIKE STENCILJS.

    View full-size slide

  21. Recipe for building good web sites

    View full-size slide

  22. Performance Budget Tools
    CALIBRE BUNDLESIZE
    SPEEDCURVE

    View full-size slide

  23. bit.ly/perf-budgets
    REAL-WORLD WEB PERF BUDGETS

    View full-size slide

  24. HEALTH OF THE
    WEB

    View full-size slide

  25. Chrome DevTools Lighthouse
    WebPageTest
    Web Performance Tooling
    Synthetic lab conditions Real-world
    RUM

    View full-size slide

  26. EXPLORE
    The health of the web as a whole with HTTP Archive

    View full-size slide

  27. BETA
    beta.httparchive.org
    AVAILABLE TODAY

    View full-size slide

  28. ✅ RESPONSE BODIES
    ✅ LIGHTHOUSE REPORTS
    ✅ BLINK FEATURE COUNTERS
    ✅ NEW PERFORMANCE METRICS
    ..AND IT’S ALL QUERYABLE!

    View full-size slide

  29. SITES ARE SENDING USERS…
    http://beta.httparchive.org
    Using Dev Tools mobile emulation, Moto G4 calibrated CPU, Cable (5/1mbps, 28ms)
    STATE OF JAVASCRIPT ON MOBILE
    P90: ~1MB
    P75: 0.6MB
    OF JS SPENDING ~4s ON PARSE/COMPILE

    View full-size slide

  30. Minify _everything_
    Babelified ES5 w/Uglify
    ES2015+ with Babili
    css-loader + minimize:true
    Code-splitting
    Dynamic import()
    Route-based chunking
    Tree-shaking
    Webpack 2+ with Uglify
    RollUp
    DCE w/ Closure Compiler
    Optimize “Vendor” libs
    NODE_ENV=production
    CommonsChunk + HashedModuleIdsPlugin()
    Transpile less code
    babel-preset-env + modules:false
    Browserlist
    useBuiltIns: true
    Scope Hoisting:
    Webpack 3
    RollUp
    Strip unused Lodash modules
    lodash-webpack-plugin
    babel-plugin-lodash
    Fewer Moment.js locales
    ContextReplacementPlugin()

    View full-size slide

  31. Is all of that ~1MB used upfront?

    View full-size slide

  32. 40%
    SITES MAY USE ONLY
    OF THE JAVASCRIPT THEY LOAD
    UPFRONT.
    With thanks to [email protected]
    JS CODE COVERAGE OF TOP 50 SITES

    View full-size slide

  33. Removing unused code can reduce network
    transmission times, CPU-intensive code
    parsing, and memory overhead

    View full-size slide

  34. SITES ARE SENDING USERS…
    http://beta.httparchive.org
    Using Dev Tools mobile emulation, Moto G4 calibrated CPU, Cable (5/1mbps, 28ms)
    STATE OF THE WEB ON MOBILE
    P50: 344KB, P75: 614KB, P90: 970KB
    P90: 5.4MB
    P75: 2.9MB
    P90 3.8MB (70%) of this is images
    P90 1MB (18%) of this is JS

    View full-size slide

  35. 70% OF THIS IS IMAGES. OPTIMIZE THEM.
    https://images.guide

    View full-size slide

  36. SITES ARE INTERACTIVE IN..
    http://beta.httparchive.org
    Using Dev Tools mobile emulation, Moto G4 calibrated CPU, Cable (5/1mbps, 28ms)
    WEB SPEED METRICS ON MOBILE
    P50: 344KB, P75: 614KB, P90: 970KB
    P90: 35s
    P75: 22s
    P90: 11s before First Meaningful Paint

    View full-size slide

  37. Queryable RUM for
    the web?
    Ilya Grigorik
    @igrigorik
    Bryan McQuade
    @bryanmcquade

    View full-size slide

  38. GIVING DEVELOPERS MORE
    control.
    @addyosmani

    View full-size slide

  39. @font-face {

    font-family: 'Roboto';

    font-display: optional;

    src: url(Roboto.woff) format('woff'),

    url(Roboto.eot) format('eot');

    font-weight: 400;

    font-style: normal;

    }
    If my Web Fonts can’t load quickly, don’t load them at all.
    Chrome 60 Safari WIP Firefox WIP

    View full-size slide

  40. // Network type that browser uses

    navigator.connection.type
    > 'wifi'

    // New: Effective connection type
    // using rtt and downlink values

    navigator.connection.effectiveType
    > '2G'
    I want to adapt serving based on estimated network quality
    BEFORE AFTER
    For more on navigator.connection.*
    See ‘Building a modern media experience’
    Chrome 62

    View full-size slide


  41. Chrome 50 Safari 11 Firefox WIP

    View full-size slide


  42. 
<br/>(async () => {
<br/>try {
<br/>const response = await fetch(new Request("movies.json", {credentials: "include"}));
<br/>const data = await response.json();
<br/>console.log(data);
<br/>} catch (exception) {
<br/>console.log("Booo");
<br/>}
<br/>})();
<br/>
    I have critical resources I want to load earlier than discovery.
    Chrome 62

    View full-size slide

  43. addEventListener('activate', event => {
    event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
    // Enable navigation preloads!
    await self.registration.navigationPreload.enable();
    }
    }());
    });
    I want to start network requests while the Service Worker is still booting up.
    Saves 1 RTT
    Early numbers suggest a
    20% improvement to page load
    time at PC95.
    Chrome 59

    View full-size slide

  44. Many sites optimize for the Lowest Common Denominator
    Most users end up being deployed ES2015 polyfills

    View full-size slide

  45. Deploying ES2015+ JavaScript in 2017
    babel-preset-env + <br/>

    View full-size slide

  46. Deploying ES2015+ JavaScript in 2017
    main-legacy.js main.js

    View full-size slide

  47. FUTURE? BETTER PERF.
    Modules.
    Service Workers.
    Navigation Architecture.
    OFF-MAIN THREAD FETCH
    SCRIPT STREAMING
    TODAY, STILL NEED TO BUNDLE FOR PRODUCTION
    PlzNavigate

    View full-size slide

  48. PROGRESSIVE WEB APPS
    ARE THE NEW
    normal.
    but…we have some new ones to share!

    View full-size slide

  49. Old Mobile Site - 1st load
    First Paint: 4.2s
    First Meaningful Paint: 6.2s
    Time To Interactive: 23s

    View full-size slide

  50. New Mobile Site - 1st load
    First Paint: 1.8s
    First Meaningful Paint: 5.1s
    Time To Interactive: 5.6s
    JS Bundles: 620KB ➡ 150KB
    CSS Bundles: 150KB ➡ 6KB inline
    P90 for Pin pages: 20s ➡ 6.5s

    View full-size slide

  51. New Mobile Site - Repeat Loads
    First Paint: 0.6s
    First Meaningful Paint: 3.5s
    Time To Interactive: 3.9s

    View full-size slide

  52. 0
    15
    30
    45
    60
    Android iOS PWA
    0.15MB
    56MB
    9.6MB
    Size: Comparing the PWA to the native apps
    150KB home feed load

    View full-size slide

  53. Comparing old mobile web to new mobile web
    +60%
    +44% +50%
    +40%
    Time Spent > 5 minutes User-generated Ad $ Ad Clickthroughs Core Engagements
    Comparing across web/native
    +2-3%
    +2% +0%
    +5%
    Time Spent > 5 minutes User-generated Ad $ Ad Clickthroughs Core Engagements

    View full-size slide

  54. JavaScript Bundle Splitting Strategy
    How did Pinterest handle code-splitting?
    Vendor
    Entry
    Async
    React, Redux, React Router etc (73KB)
    Main shell, Core logic, Redux store (59KB)
    Async route chunks (13-18KB)

    View full-size slide

  55. const chunkPlugins = [

    new webpack.optimize.CommonsChunkPlugin({

    name: 'vendor-mweb',

    minChunks: Infinity,

    chunks: ['entryChunk-mobile']

    }),

    new webpack.optimize.CommonsChunkPlugin({

    name: 'entryChunk-webpack',

    minChunks: Infinity,

    chunks: ['vendor-mweb']

    }),

    ];
    const bundles = {

    'vendor-mweb': [

    'app/mobile/polyfills.js',

    'intl',

    'normalizr',

    'react-dom',

    'react-redux',

    'react-router-dom',

    'react',

    'redux'

    ],

    'entryChunk-webpack': 'app/mobile/runtime.js',

    'entryChunk-mobile': 'app/mobile/index.js'

    };
    Webpack Config

    View full-size slide

  56. // Create a loader

    const Closeup = () => import(/* webpackChunkName: "CloseupPage" */
    'app/mobile/routes/CloseupPage');


    // Register it to the route

    route('/pin/:pinId', routes.Closeup, { name: 'Closeup' }),


    // Render a react-router-v4 Route with the route bundle loader


    bundleLoader={loader}

    routeName={name}

    {...matchProps}

    {...props}

    />}

    />
    React Router V4

    View full-size slide

  57. // Async load the route bundle

    class PageRoute extends PureComponent {

    render() {

    const { bundleLoader, ...props } = this.props;

    return ;

    }

    }


    // Load it and render

    class Loader extends PureComponent {

    componentWillMount() {

    this.props.loader().then(module => {

    this.setState({ LoadedComponent: module.default });

    });

    }

    }
    React Router V4

    View full-size slide

  58. Webpack Bundle Analyzer: Before splitting out common async route code

    View full-size slide

  59. Webpack Bundle Analysis
    Tackling dupes across lazily loaded routes
    - Moved common code into the Entry chunk
    - 20% increase in size of Entry (59KB -> 71KB)
    - 60-90% decrease in size of async route chunks
    - e.g: Homefeed (13.9KB -> 1KB), Closeup (18KB -> 7KB)

    View full-size slide

  60. Webpack Bundle Analyzer: After moving out common code from async chunks into entryChunk
    60-90% decrease in size of async route chunks (e.g 13.9KB ➡ 1KB)
    20% increase in size of entry (59KB ➡ 71KB)

    View full-size slide

  61. Service Workers
    Caching runtime & static assets offline
    Start
    - Runtime caching async JS chunks (for V8 bytecode cache)
    - Precaching vendor & entry chunks
    - Precaching most used routes (e.g Pins)
    - Generating a SW for each locale bundle
    Today
    - Cache all JS/CSS cache-first
    - Cache the Application Shell
    - Precache bundle loaded by the shell
    - Webpack runtime, vendor, entry
    - Named chunks to cache async routes

    View full-size slide

  62. /* global $VERSION, $Cache, importScripts, WorkboxSW */

    importScripts('https://unpkg.com/[email protected]/build/importScripts/workbox-sw.prod.v1.1.0.js');


    // Add app shell to the webpack-generated precache list

    $Cache.precache.push({ url: 'sw-shell.html', revision: $VERSION });


    // Register precache list with Workbox

    const workbox = new WorkboxSW({ handleFetch: true, skipWaiting: true, clientClaim: true });

    workbox.precache($Cache.precache);


    // Runtime cache all js

    workbox.router.registerRoute(/webapp\/js\/.*\.js/, workbox.strategies.cacheFirst());


    // Prefer app-shell for full-page loads

    workbox.router.registerNavigationRoute('sw-shell.html', {

    blacklist: [

    // bunch of non-app routes

    ],

    });
    SW Caching

    View full-size slide

  63. Future
    - Web Push notifications
    - Fixing slow API responses (home-feed takes 1s on Fast 3G)
    - Optimize server latency and response sizes
    - Adding for preloading bundles

    View full-size slide

  64. 0
    7.5
    15
    22.5
    30
    Android PWA
    2.8MB
    30MB
    Size: Comparing the PWA to the native apps

    View full-size slide

  65. Performance Budgets
    Budgets Tinder tries not to exceed
    Vendor
    Async
    Other
    155KB
    55KB
    35KB
    CSS 20KB

    View full-size slide

  66. import A from '../A';

    import B from '../B';

    const route = [

    {

    route: '/',

    regions: {

    side: A,

    main: B

    }

    }

    ];
    JavaScript Route-based code-splitting
    Before
    main.js
    A B

    View full-size slide

  67. import Loadable from ‘react-loadable';

    const A = Loadable({

    loader: () => import('../A' /* webpackChunkName: "pc-r-A" */),

    loading: () => null

    });

    const B = Loadable({

    loader: () => import('../B' /* webpackChunkName: "pc-r-B" */),

    loading: () => null

    });


    const route = [

    {

    route: '/',

    regions: {

    side: A,

    main: B

    },

    preload: [ /* next page chunk to preload*/ ]

    }
    React Router
    React Loadable
    CommonsChunkPlugin
    After
    A.js
    B.js

    View full-size slide

  68. const LoadableComponent = Loadable({...});


    LoadableComponent.preload();
    JavaScript Route-based code-splitting
    Preloading more page chunks with React Loadable

    View full-size slide

  69. JavaScript Route-based code-splitting
    Before
    Main bundle size: 166kb
    DOMContentLoad: 5.46s
    load: 11.91s
    After
    Main bundle size: 101kb
    DOMContentLoad: 4.69s
    load: 4.69s

    View full-size slide

  70. before
    after Reduce first paint by 500ms, load time by 1 second

    View full-size slide

  71. new BundleAnalyzerPlugin({

    analyzerMode: 'server',

    analyzerPort: 8888,

    reportFilename: 'report.html',

    openAnalyzer: true,

    generateStatsFile: false,

    statsFilename: 'stats.json',

    statsOptions: null

    })
    Webpack Bundle Analysis
    ✅ core-js + babel-preset-env to drop unused polyfills
    ✅ Use lodash-webpack-plugin to reduce bundle size
    ✅ Replaced localForage with IndexedDB
    ✅ Split non-critical components not used for First Paint
    ✅ Removed critical CSS from bundle (SSRs already)

    View full-size slide

  72. new LodashModuleReplacementPlugin({

    caching: true,

    collections: true,

    paths: true,

    shorthands: true

    }),
    Lodash Savings

    View full-size slide

  73. CSS Loading Strategy
    before after

    View full-size slide

  74. new webpack.optimize.ModuleConcatenationPlugin(),

    new webpack.DefinePlugin({

    'process.env': {

    // Env stuff...

    },
    Update to Webpack 3 & React 16
    Webpack 2 -> 3 reduced JS
    parsing time by 8% (250ms
    -> 230ms)
    React 15.x -> React 16:
    reduced vendor chunk
    size by ~6.7%

    View full-size slide

  75. new workboxWebpackPlugin({

    injectManifest: true,

    swSrc: paths.serviceWorkerSrcPath,

    swDest: paths.serviceWorkerDestPath,

    globDirectory: paths.staticPath,

    globPatterns: [

    '**/*.{js,html,css,svg,woff2}'

    ],

    templatedUrls: {

    'index.html': [

    '../public/static/build/main-*.js',

    '../public/static/build/vendor-*.js',

    '../public/static/build/manifest-*.js',

    '../public/static/build/style.*.css'

    ]

    },

    maximumFileSizeToCacheInBytes: 4194304

    })]

    View full-size slide

  76. IMPROVING PERFORMANCE IS A
    JOURNEY. LOTS OF SMALL CHANGES
    CAN LEAD TO BIG GAINS.

    View full-size slide

  77. MEASURE mom.
    OPTIMIZE
    MONITOR

    View full-size slide

  78. Links
    Thanks!
    Web Fundamentals
    developers.google.com/web
    Chrome User Experience Report
    bit.ly/introducing-crux
    React Perf Case Studies
    medium.com/@addyosmani
    Addy Osmani
    @addyosmani
    Ilya Grigorik
    @igrigorik
    Bryan McQuade
    @bryanmcquade
    Come chat with us at the demo area!

    View full-size slide