Bandwidth Friendly
tips & tricks
Why Tho?
Bandwidth Costs
- end users must pay
- content providers must pay
cost for ~750kB payload
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
RAIL Performance Model
https://developers.google.com/web/fundamentals/performance/rail
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
- don't mess up
- 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
- for example: one image
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
Bandwidth Friendly
By Jared Anderson
Bandwidth Friendly
- 1,323