Bandwidth Friendly

tips & tricks

Why Tho?

Bandwidth Costs

  • end users must pay
  • content providers must pay

cost for ~750kB payload

https://whatdoesmysitecost.com

Varying Speed Capability

High

26.7 Mbps

 

US

14.2 Mbps

 

Average

5.6 Mbps

 

Low

0.34 Mbps

Performance & UX

 

varying bandwidth capabilities and differing payload sizes complicates performance goals

 

the RAIL performance model

  • Respond to user input in 50ms
  • Load content within 5s

Usage Caps

  • most locations have data plans with finite limits
  • some locations only have data plans with finite limits

Balancing Tradeoffs

Bandwidth

is just one of many factors to consider when developing a product and never the most important

Product Requirements

every product has a purpose - each demands different payload requirements

User Experience

navigating a user to fulfill product's purpose has a payload cost

Performance

sometimes to achieve performance benchmarks, (particularly perceived performance), larger payloads are required

Money Limitations

handcrafting every view with exact precision could yield lower payloads but at extremely production high costs

Stable, Mature, Reusable Code

proven code yields less bugs but often with bigger payloads

Longterm Maintainability

using common patterns, libraries or frameworks often age better but potentially with higher payload costs

Content Discoverability (SEO)

server-side rendering everything potentially increases payload (markup generally more verbose than JSON data)

Accessibility

developing accessible features can result in increased payloads sizes

Deadlines

payload optimizations can take time

Brand / Legal Compliance

often requires increases in payload

and more, but...

😞

😏

great. yet another thing I have to juggle and worry about.

yes! finally some ammo to kill that _______ I never wanted to use anyway

don't be these guys

developers

educate & inform

 

product managers

balance tradeoffs, prioritize, decide

Understanding

True of False?

Being bandwidth friendly is simple: just send down less data.

Being bandwidth friendly is simple: just sending down less data.

True*

interaction

as some user-initiated event which causes the view to update in a meaningful way

for the sake of this presentation, let's define...

  • navigating between pages / routes
  • opening a drawer / menu
  • scrolling vertically or horizontally 😈
  • changing tabs
  • paginating through result-sets
  • cycling through a carousel
  • opening a modal
  • etc

Consider...

Start on View 1

  • no scrolling
  • just input form

End on View 2

  • no scrolling

NOT MUCH ROOM TO OPTIMIZE

only 1 possible interaction

strategy: don't mess up

vs...

Complex Landing Page

  • Scrolls Vertically
  • 10+ slides
    • image
    • CTA
  • Intro content
    • image
    • title
    • text
  • Tabbed Comments Widget
    • view comments
    • leave comments

LOTS OF ROOM TO OPTIMIZE

(12+ interactions excluding scrolling)

strategy: don't mess up, optimize assets, tailor to device capabilities, and lazy-load expensive parts

Best, Bad Analogy

  • content / app never interacted with all at once
  • viewed through small lens
  • lens of different sizes per device
  • user can move lens in any direction / order
  • entire "board" is excessively heavy - sometimes unmeasurably so and should be loaded only on-demand

non-evil ouija gif

Fun Fact

 

In most every real-world context, end-users will not interact with every possible interaction provided by your app...

sending down data for unused features is bloat...

adding features which defer & smartly load content will increase your app's potential payload but will reduce its effective payload

"add more to send less"

So, What to Understand

  • the expensive parts
    • non-optimized stuff
    • whimsical addition of libs
    • media: images, video, audio
    • fonts
    • 3rd-party integrations
    • complex UI
  • nature of app / app features
    • interactions causing significant view transitions
    • planned user-flows / traffic patterns / UX
  • user behavior
    • most common interactions on a particular view
    • how users nav between views
    • bounce rates

Bandwidth Friendly

Being bandwidth friendly is about minimizing the payload and using various strategies to postpone, tailor & avoid sending data to end-users

 

  1. don't mess up
  2. strategically optimize

Budgeting

Definition of Good

 

without defining a bandwidth budget, you can't know good from bad

Per-URL Budgets

initial load payload limits should be based around product goals & requirements, but a good baseline:

 

< 500K 💰

 

< 750K 😀

 

< 1MB  😐

 

> 1MB  🤨

Go Granular

on your budgeting because all 30KBs aren't equal:

 

30KB Photo 💰

 

30KB Platform Framework  😀

 

30KB Font  😐

 

30KB date-formatting lib  🤨

 

30KB Icon 🙃

make it a product requirement

get the whole team on-board

💥

Measuring

Dev Tools Network Tab

  • load while emulating multiple devices
  • disable cache
  • enable view of gzipped sizes

 

(show quick demo)

Bundle Phobia

  • See what NPM modules will roughly cost when bundled into app

Webpack Bundle Analyzer

  • See what is weighing down your app

Editor Plugins

vs code plugin: "Import Cost"

CI Tools

make it a part of your QA process

💥

How Tho

part 1: don't mess up

Think

it goes a long way™

Before Adding to App, Ask:

  • Do I really need this?
  • What does this weigh?
  • What does this do to my budget?
  • How can this be optimized?

G'zipped Responses

What is it?

server configuration that returns zipped content if the browser asks for it

HTTP Headers

after gzip

before gzip

Results

Web Server Config

import express from "express";
import compression from "compression";

const app = express();

app.use(compression());

app.get("/hello", (req, res) => {
  res.send("hello");
});

app.listen(3000, () => console.log("Example app listening on port 3000!"));

Cache Headers

Cache-Control

 

  • an HTTP Header
  • tells browsers how long assets should be considered valid per URL
  • browsers optimize subsequent requests to same URL

Cache Headers

the header

first request

subsequent request

Web Server Config

import express from "express";

const app = express();

app.use((req, res, next) => {
  res.set("Cache-Control", `public, max-age=${60*60*24*7}`);
  next();
});

app.get("/hello", (req, res) => {
  res.send("hello");
});

app.listen(3000, () => console.log("Example app listening on port 3000!"));

Optimization & Minification

Condenses Verbose Code

const add5Things = (
  parameterNumber1,
  parameterNumber2,
  parameterNumber3,
  parameterNumber4,
  parameterNumber5
) =>
  parameterNumber1 +
  parameterNumber2 +
  parameterNumber3 +
  parameterNumber4 +
  parameterNumber5;
const add5Things = (a, b, c, d, e) =>
  a + b + c + d + e;

before

after

Built into Most Modern Build Systems

  • Webpack
  • Babel Minify
  • Uglify

Testing

can you read the code?

not minified

maybeminified

yes

no

Not just for JS Code

minification and optimization tools exist for all asset types:

  • imagemin
  • svgo
  • uglifycss
  • html-uglify

How Tho

part 2: optimize

CDN

CDN

  • content delivery network
  • for static assets
    • JS files, CSS files, images, static HTML, etc.
  • places content regionally near user via replication
  • typically on different domain than origin

CDN & Bandwidth

  • placing common libraries on common CDN increases likelihood of assets already being cached
  • placing assets on separate domain than origin reduces amount of HTTP headers sent with every request

Public CDN

Internal CDN

Responsive Media

one web app

  • served to phones, TVs & all in between
  • device widths change drastically
  • designs changes per device
    • for example: one image
      • full width in one context
      • partial width in another
      • not visible in another

The Cost of Images

Dimensions of BG Image Size in kB
1900 x 1280 828 KB
1600 x 1067 569 KB
1200 x 800 337 KB
1000 x 667 241 KB
800 x 534 152 KB
600 x 401 92 KB
400 x 267 42 KB

lots of bandwidth savings to be had by sending the right image for the user's device

Responsive CSS

  • for decorative images
.hero {
  background: url("./400x267.jpeg");

  @media (min-width: 400px) {
     background: url("./600x401.jpeg");
  }

  @media (min-width: 800px) {
     background: url("./800x534.jpeg");
  }
}

HTML Image Source Set & Sizes

  • for semantic images
  • tell browser specs of images sources
    • path
    • width or pixel density
  • tell browser the context
    • how much room image should take per media query
  • browser picks best image per device context
<img
  srcset="./800x534.jpeg 800w,
   ./600x401.jpeg 600w,
   ./400x267.jpeg 400w"
  sizes="(min-width: 800px) 50vw, 100vw"
/>

HTML Picture element

  • similar to srcset + sizes
  • often used for art direction
<picture>
 <source
    srcset="mdn-logo-wide.png"
    media="(min-width: 600px)">
 <img src="mdn-logo-narrow.png" alt="MDN">
</picture>

Can I Use?

yes you can

April 2018

Web Fonts

System Fonts

  • most bandwidth friendly
  • chances of that happening: 0%

Custom Fonts 

  • Increased payload weight for every combination of weight, style & family, eg:
    • light, italic, "Open Sans"
    • normal, italic, "Open Sans"
    • bold, italic, "Open Sans"
    • light, normal, "Open Sans"
    • normal, normal, "Open Sans"
    • bold, normal, "Open Sans"
    • etc
  • There are 9 potential weights, 3 potential styles, and unlimited families 😱
  • Each weighing in at 10kB - 40kB

Collaborate with Design

  • explain tradeoffs
  • agree on budget

Fonts: Lazy by Default

@font-face { 
    font-family: MyHelvetica; 
    src: local("Helvetica Neue Bold"),
         local("HelveticaNeue-Bold"),
         url(./MgOpenModernaBold.ttf);
    font-weight: bold;
    unicode-range: U+0025-00FF;
} 

font wont load until/unless a css rule matches

Cached & Offline Assets

Service Worker Cache API

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(
        [
          '/static/main.css',
          '/static/main.js',
          '/offline.html'
        ]
      );
    })
  );
});

Can I Use?

yeah, pretty much

April 2018

Cached Data

Service Worker Cache API

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

Tailored Data

View 1

[
  {
    id: "1234",
    name: "Jane Doe",
    profileUrl: "https://example.org/u/1234",
    profilePic: "https://example.org/pics/selfie.jpg",
    dob: "1900-01-01",
    hairColor: "red",
    eyeColor: "green",
    lastLocation: {
      city: "South Jordan",
      state: "UT"
    },
    favs: {
        color: "red",
        food: "tacos",
        transportation: "horse",
        animal: "dog",
        game: "twister"
    }
  },
  {
    id: "abcd",
    name: "John Doe",
    profileUrl: "https://example.org/u/abcd",
    profilePic: "https://example.org/pics/ugly.jpg",
    dob: "1958-09-01",
    hairColor: "brown",
    eyeColor: "green",
    lastLocation: {
      city: "South Jordan",
      state: "UT"
    },
    favs: {
        color: "red",
        food: "tacos",
        transportation: "horse",
        animal: "dog",
        game: "twister"
    }
  }
];
[
  {
    id: "1234",
    name: "Jane Doe",
    profileUrl: "https://example.org/u/1234",
    profilePic: "https://example.org/pics/selfie.jpg"
  },
  {
    id: "abcd",
    name: "John Doe",
    profileUrl: "https://example.org/u/abcd",
    profilePic: "https://example.org/pics/ugly.jpg"
  },
  {
    id: "8192",
    name: "Jay Doe",
    profileUrl: "https://example.org/u/8192",
    profilePic: "https://example.org/pics/pic.jpg"
  },
  {
    id: "efgh",
    name: "Judy Doe",
    profileUrl: "https://example.org/u/efgh",
    profilePic: "https://example.org/pics/judy.jpg"
  }
];

endpoint provides

view needs

GraphQL

allUsers {
  id
  name
  profileUrl
  profilePic
}

View 2

[
  {
    id: "1234",
    name: "Jane Doe",
    favs: {
      food: "tacos"
    }
  },
  {
    id: "abcd",
    name: "John Doe",
    favs: {
      food: "pizza"
    }
  },
  {
    id: "8192",
    name: "Jay Doe",
    favs: {
      food: "fish"
    }
  },
  {
    id: "efgh",
    name: "Judy Doe",
    favs: {
      food: "nope"
    }
  },
  {
    id: "zyzyz",
    name: "Justine Doe",
    favs: {
      food: "candy"
    }
  }
];

endpoint provides

view needs

[
  {
    id: "1234",
    name: "Jane Doe",
    profileUrl: "https://example.org/u/1234",
    profilePic: "https://example.org/pics/selfie.jpg",
    dob: "1900-01-01",
    hairColor: "red",
    eyeColor: "green",
    lastLocation: {
      city: "South Jordan",
      state: "UT"
    },
    favs: {
        color: "red",
        food: "tacos",
        transportation: "horse",
        animal: "dog",
        game: "twister"
    }
  },
  {
    id: "abcd",
    name: "John Doe",
    profileUrl: "https://example.org/u/abcd",
    profilePic: "https://example.org/pics/ugly.jpg",
    dob: "1958-09-01",
    hairColor: "brown",
    eyeColor: "green",
    lastLocation: {
      city: "South Jordan",
      state: "UT"
    },
    favs: {
        color: "red",
        food: "tacos",
        transportation: "horse",
        animal: "dog",
        game: "twister"
    }
  }
];

GraphQL Clients

 

such as Apollo are sophisticated enough to know you already have user name and will optimize the request to avoid duplication

Tree Shaking

Dead Code Elimination

export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}
import { cube } from './math.js';

console.log(cube(3));

./math.js

./app.js

dead code

  • Detects and removes unused exports
  • Requires static import / export
  • Requires sideEffects marker (webpack 4)
    • module authors add it package.json
    • module consumers to webpack.config.js

Lazy

Lazy By Default

  • never* have we loaded an entire web app at once
  • in the beginning, we've used page transitions as the event indicating when to load more content
  • this route-based lazy-loading of content we got without doing much
  • but there are many more UI events we can leverage to refine the lazy-loading behavior

 

Lazy Content

Below the Fold

  • How much data have we wasted on content which people never scroll to?

like these images, for example?

Offscreen Content

  • How much data have we wasted on carousel content never cycled to?

like slides 2-10 for example?

If only there was an event for content entering / exiting the viewport

🤔

Intersection Observer

💥

var options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

var observer = new IntersectionObserver(callback, options);

var target = document.querySelector('#listItem');
observer.observe(target);

Use that event to delay rendering

const callback = ([entry]) => {
  const stub = entry.target;
  stub.innerHTML = `<img src="3mb-image.jpg" />`;
};

Eden Lazy

import Lazy from '@lds/eden-lazy'

<Lazy>
  <img src="/path/to/3mb.jpg" alt="big image" />
</Lazy>

or just use

Lazy Data

Data on Lifecycle

  • only fetch data when the component housing it "mounts"

 like the currently hidden, view comments pane

Mount Fetch

{this.state.pane === "comments" && (
    <Fetch
      url={`/api/comments/${id}`
      render={({ data }) =>
        data && <CommentsPane {...data} />
      }
    />
)}

Fetch Component

import React from "react";

export default class Fetch extends React.Component {
  state = {
    data: null
  };

  componentDidMount() {
    fetch(this.props.url).then(res =>
      this.setState({
        data: res.json()
      })
    );
  }

  render() {
    return this.props.render(this.state);
  }
}

Lazy Code

Conditional Code

  • only load in heavy-but-necessary third-party code when user shows intent to use it

 like this 3rd-party, rich-text editor

Next Dynamic

conditional code splitting & loading of any importable module

import dynamic from "next/dynamic";
import Lazy from "@lds/eden-lazy";

const LazyQuill = dynamic(import("react-quill"));

// handwave

<Lazy>
  <LazyQuill value={this.state.text} />
</Lazy>

The End