Fast By Default: Modern loading best practices

96270e4c3e5e9806cf7245475c00b275?s=47 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

96270e4c3e5e9806cf7245475c00b275?s=128

Addy Osmani

October 27, 2017
Tweet

Transcript

  1. 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?
  2. 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
  3. 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
  4. 9.
  5. 11.
  6. 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
  7. 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.
  8. 16.

    * Chrome has 4+ caches. The above reflects the main

    two - the HTTP and memory caches Chrome’s Cache Hit Rates
  9. 18.

    5s

  10. 19.
  11. 20.
  12. 21.

    “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
  13. 22.
  14. 24.

    GOOD OPTIONS FOR MOBILE WEB POLYMER PREACT VUE.JS GLIMMER SVELTE

    REACT *WITH CODE-SPLITTING AND A PERF BUDGET AND OTHERS, LIKE STENCILJS.
  15. 32.

    ✅ RESPONSE BODIES ✅ LIGHTHOUSE REPORTS ✅ BLINK FEATURE COUNTERS

    ✅ NEW PERFORMANCE METRICS ..AND IT’S ALL QUERYABLE!
  16. 33.

    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
  17. 34.

    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()
  18. 36.

    40% SITES MAY USE ONLY OF THE JAVASCRIPT THEY LOAD

    UPFRONT. With thanks to fmeawad@chromium.org JS CODE COVERAGE OF TOP 50 SITES
  19. 38.

    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
  20. 40.

    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
  21. 41.
  22. 43.
  23. 44.
  24. 45.
  25. 46.
  26. 47.
  27. 48.
  28. 49.
  29. 50.
  30. 51.
  31. 52.
  32. 53.
  33. 54.
  34. 55.
  35. 56.
  36. 57.
  37. 58.
  38. 59.
  39. 60.
  40. 61.
  41. 62.
  42. 63.
  43. 64.
  44. 65.
  45. 66.
  46. 67.
  47. 69.

    @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
  48. 70.

    // 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
  49. 71.
  50. 73.

    <link rel="preload" href="movies.json" as="fetch" crossorigin="use-credentials">
 <script>
 (async () => {


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

    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
  52. 75.
  53. 78.

    FUTURE? BETTER PERF. Modules. Service Workers. Navigation Architecture. OFF-MAIN THREAD

    FETCH SCRIPT STREAMING TODAY, STILL NEED TO BUNDLE FOR PRODUCTION PlzNavigate
  54. 80.
  55. 81.
  56. 82.
  57. 83.

    Old Mobile Site - 1st load First Paint: 4.2s First

    Meaningful Paint: 6.2s Time To Interactive: 23s
  58. 84.

    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
  59. 85.

    New Mobile Site - Repeat Loads First Paint: 0.6s First

    Meaningful Paint: 3.5s Time To Interactive: 3.9s
  60. 86.

    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
  61. 87.

    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
  62. 88.

    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)
  63. 89.

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

    // 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
 <Route exact key="matched-route" path={path} render={matchProps =>
 <PageRoute
 bundleLoader={loader}
 routeName={name}
 {...matchProps}
 {...props}
 />}
 /> React Router V4
  65. 91.

    // Async load the route bundle
 class PageRoute extends PureComponent

    {
 render() {
 const { bundleLoader, ...props } = this.props;
 return <Loader loader={bundleLoader} {...props} />;
 }
 }
 
 // Load it and render
 class Loader extends PureComponent {
 componentWillMount() {
 this.props.loader().then(module => {
 this.setState({ LoadedComponent: module.default });
 });
 }
 } React Router V4
  66. 93.

    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)
  67. 94.

    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)
  68. 95.

    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
  69. 96.

    /* global $VERSION, $Cache, importScripts, WorkboxSW */
 importScripts('https://unpkg.com/workbox-sw@1.1.0/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
  70. 97.

    Future - Web Push notifications - Fixing slow API responses

    (home-feed takes 1s on Fast 3G) - Optimize server latency and response sizes - Adding <link rel=preload as=script> for preloading bundles
  71. 98.
  72. 99.

    0 7.5 15 22.5 30 Android PWA 2.8MB 30MB Size:

    Comparing the PWA to the native apps
  73. 102.

    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
  74. 103.

    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
  75. 105.

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

    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)
  77. 110.

    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%
  78. 111.

    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
 })]
  79. 114.

    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!