$30 off During Our Annual Pro Sale. View Details »

Fast by Default: Modern Loading Practices

Fast by Default: Modern Loading Practices

This presentation is a modified version of Addy Osmani's "Fast By Default" presentation from Chrome Dev Summit 2017 (https://www.youtube.com/watch?v=_srJ7eHS3IM)

Bilal Çınarlı

June 06, 2018
Tweet

More Decks by Bilal Çınarlı

Other Decks in Technology

Transcript

  1. Fast By Default
    Modern Loading Best Practices

    View Slide

  2. Bilal Çınarlı
    Frontend Architect
    Software Engineer @Adidas
    @bcinarli
    github.com/bcinarli
    bcinarli.com

    View Slide

  3. View Slide

  4. First Paint
    First Meaningful
    Paint
    Time To Interactive
    USER-HAPPINESS

    View Slide

  5. How to be fast?
    By following best practices
    also, investing in your code!

    View Slide

  6. Mobile Web Delivery Best Practices
    Compress! (GZip, Brotli)
    Preresolve DNS
    Preload critical resources
    Cache effectively
    Respect Data Plans
    Stream HTML
    ⚡ Minify & Optimize *.*
    ✋ SSR is not a panacea
    ✂ Code-splitting
    Route-based chunking
    Library sharding
    PRPL Pattern
    Tree-shaking
    Differential serving
    / Scope Hoisting
    Don’t ship DEV to PROD
    *.js
    *.*

    View Slide

  7. Improving your performance
    and applying best practices
    is a journey where small changes
    can lead to significant achievements

    View Slide

  8. Being Progressive

    View Slide

  9. View Slide

  10. You can load instantly in any network,
    even pre-caching resources to
    reduce the need for the network!

    View Slide

  11. View Slide

  12. Become ridiculously fast!
    Once loaded,
    users expect pages to be fast.

    View Slide

  13. INVESTMENTS IN SPEED

    View Slide

  14. Where do mobile sites spend their time loading?
    With thanks to Camillo and Mathias @ V8

    View Slide

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

    View Slide

  16. JavaScript Parse Cost On Mobile - CNN
    ~9s difference to the A11

    View Slide

  17. Follow Patterns

    View Slide

  18. PRPL Pattern
    INSPIRED

    View Slide

  19. Use Your Budget Wisely

    View Slide

  20. View Slide

  21. View Slide

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

    View Slide

  23. View Slide

  24. View Slide

  25. Change Your Coding Style

    View Slide

  26. WebPack is a module bundler
    which has lots of perks.
    Code Splitting,
    Chunking,
    Tree-shaking…

    View Slide

  27. View Slide

  28. Create a Lab Environment

    View Slide

  29. Chrome DevTools Lighthouse WebPageTest

    View Slide

  30. Use New Techniques Now!

    View Slide

  31. If Web Fonts don’t load quickly,
    don’t load them at all.

    View Slide

  32. @font-face {
    font-family: "Roboto";
    font-display: optional;
    src: url("Roboto.woff") format("woff"),
    url("Roboto.eof") format("eot");
    font-weight: 400;
    font-style: normal;
    }

    View Slide

  33. I want to adapt serving
    based on network quality

    View Slide

  34. // Network type that browser uses
    navigator.connection.type
    > ‘wifi’
    // New: effective connection type
    // using rtt and downlink values
    navigator.connection.effectiveType
    > ‘2G’

    View Slide

  35. I have a critical resource,
    I want to load earlier than the discovery

    View Slide


  36. <br/>(async () => {<br/>try {<br/>const response = await fetch("movies.json");<br/>const data = await response.json();<br/>console.log(data);<br/>} catch (e) {<br/>console.log("Booo");<br/>}<br/>})();<br/>

    View Slide

  37. I want to start network requests
    while Service Worker is still booting up

    View Slide

  38. addEventListener('activate', event => {
    event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
    // Enable navigation preloads!
    await self.registration.navigationPreload.enable();
    }
    }());
    });

    View Slide

  39. View Slide

  40. View Slide

  41. 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 Slide

  42. 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 Slide

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

    View Slide

  44. New Mobile Site - 1st load
    First Paint: 1.8s
    First Meaningful Paint: 5.1s
    Time To Consistently Interactive: 5.6s

    View Slide

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

    View Slide

  46. Performance
    20s ➡ 6.5s
    P90 for Pin pages
    13s ➡ 4s
    P50 for Pin pages
    620KB ➡ 150KB
    JS Bundles
    150KB ➡ 6KB inline
    CSS Bundles

    View Slide

  47. JavaScript Bundle Splitting Strategy
    How did Pinterest handle code-splitting?
    - Chunk 1: Vendor (react, redux, react-router etc)
    - Chunk 2: Entry (main shell, core logic, redux store)
    - Chunk 3: Async route chunks
    - Sizes: Vendor (~73KB), Entry (59KB), Async (13-18KB)

    View Slide

  48. 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']
    }),
    new webpack.optimize.CommonsChunkPlugin({
    children: true,
    name: 'entryChunk-mobile',
    minChunks: (module, count) => {
    return module.resource &&
    (isCommonLib(resource) || count >= 3);
    }
    })];
    Webpack Config
    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'
    };

    View Slide

  49. // 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
    render={
    matchProps =>
    bundleLoader={loader}
    routeName={name}
    {...matchProps}
    {...props} />
    }/>
    React Router V4

    View Slide

  50. // 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 Slide

  51. Webpack Bundle Analysis
    Using webpack-bundle-analyzer

    View Slide

  52. Duplicate code in the async chunk
    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 Slide

  53. Before splitting out common async route code

    View Slide

  54. After moving out common code from async chunks into entryChunk:

    View Slide

  55. Service Worker Caching Journey
    Caching runtime & static assets offline
    Start
    - Runtime caching async JS chunks
    - 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 bundled loaded by the shell
    - Webpack runtime, vendor, entry
    - Named chunks to cache async routes

    View Slide

  56. /* 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 Slide

  57. Application Shell Pattern
    Caching Pinterest’s UI assets for instant repeat loads
    - Tricky: historical initial payloads contained user
    experiments, user info, contextual info. Cache?
    Take a perf hit of render blocking request before
    rendering anything?
    - Decided to cache this in the AppShell. Managed
    when to “invalidate” the shell
    - Each request has an app version. If the version
    changes, unregister the SW, register the new one.
    - Experiment groups cache in localStorage and
    update manually.

    View Slide

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

    View Slide

  59. View Slide

  60. Before

    View Slide

  61. After

    View Slide

  62. Performance Budgets
    Budgets Tinder tries not to exceed
    - Main/vendor chunk: 155KB
    - Async common chunks: 55KB
    - Other chunks: 35KB
    - CSS hard limit of 20KB

    View Slide

  63. import A from '../A';
    import B from '../B';
    const route = [{
    route: '/',
    regions: {
    side: A,
    main: B
    }
    }];
    JavaScript Route-based code-splitting
    Before

    View Slide

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

    View Slide

  65. const LoadableComponent = Loadable({…});
    LoadableComponent.preload();
    JavaScript Route-based code-splitting
    Preloading more page chunks with React Loadable

    View Slide

  66. JavaScript Route-based code-splitting
    Before
    Main bundle size: 166kb
    DOMContentLoad: 5.46s
    load: 11.91s

    View Slide

  67. JavaScript Route-based code-splitting
    After
    Main bundle size: 101kb
    DOMContentLoad: 4.69s
    load: 4.69s

    View Slide

  68. Improve JavaScript caching
    Trim down vendor/library invalidation
    Use a whitelist of external deps
    & split out Webpack manifest
    from main chunk. Bundle now
    ~160KB for both chunks.
    module.exports = [
    'immutable',
    'lodash',
    'lodash-es',
    'react',
    'react-dom',
    'react-redux',
    'react-router',
    'redux',
    'redux-persist',
    'redux-immutable',
    'redux-saga',
    ];

    View Slide

  69. before
    after

    View Slide

  70. new BundleAnalyzerPlugin({
    analyzerMode: 'server',
    analyzerPort: 8888,
    reportFilename: 'report.html',
    openAnalyzer: true,
    generateStatsFile: false,
    statsFilename: 'stats.json',
    statsOptions: null
    })
    Webpack Bundle Analysis

    View Slide

  71. new LodashModuleReplacementPlugin({
    caching: true,
    collections: true,
    paths: true,
    // replace find/findIndex later
    // https://github.com/lodash/lodash-webpack-plugin/issues/79
    shorthands: true
    })

    View Slide

  72. Bundle Analysis: Login Page

    View Slide

  73. CSS Loading Strategy
    Atomic CSS
    - Using Atomic CSS to create highly reusable CSS styles
    - All atomic CSS style are inlined in the initial paint
    - Some CSS loaded in stylesheet (including animations)
    before after

    View Slide

  74. Runtime Performance
    Beacons and Swipes
    - requestIdleCallback() defer non-critical actions into idle
    - Simplified HTML composite layers to reduce paint counts

    View Slide

  75. new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.DefinePlugin({
    'process.env': {
    // Env stuff...
    }),
    Update to Webpack 3
    JavaScript Parsing Time: 250ms -> 230ms (8%)
    React 15.x -> React 16 reduced vendor chunks by ~6.7%

    View Slide

  76. 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 Slide

  77. Resources

    View Slide

  78. • Fast By Default: Modern Loading Practices (https://goo.gl/CnxikT)
    • JavaScript Startup Performance (https://goo.gl/HHzUSh)
    • The Cost-of JavaScript (https://goo.gl/yv7j2j)
    • Pinterest PWA Case Study (https://goo.gl/PVBjFW)
    • Tinder PWA Case Study (https://goo.gl/vrtjFi)
    • Chrome User Experience Report (https://goo.gl/pJpYUx)
    • User Centric Performance Metrics (https://goo.gl/kKRduJ)
    • Can You Afford It?: Real-world Web Performance Budgets (https://
    goo.gl/WyGNs3)

    View Slide

  79. Thank you
    @bcinarli

    View Slide