tips & tricks
cost for ~750kB payload
High
26.7 Mbps
US
14.2 Mbps
Average
5.6 Mbps
Low
0.34 Mbps
varying bandwidth capabilities and differing payload sizes complicates performance goals
the RAIL performance model
RAIL Performance Model
https://developers.google.com/web/fundamentals/performance/rail
is just one of many factors to consider when developing a product and never the most important
every product has a purpose - each demands different payload requirements
navigating a user to fulfill product's purpose has a payload cost
sometimes to achieve performance benchmarks, (particularly perceived performance), larger payloads are required
handcrafting every view with exact precision could yield lower payloads but at extremely production high costs
proven code yields less bugs but often with bigger payloads
using common patterns, libraries or frameworks often age better but potentially with higher payload costs
server-side rendering everything potentially increases payload (markup generally more verbose than JSON data)
developing accessible features can result in increased payloads sizes
payload optimizations can take time
often requires increases in payload
😞
😏
great. yet another thing I have to juggle and worry about.
yes! finally some ammo to kill that _______ I never wanted to use anyway
developers
educate & inform
product managers
balance tradeoffs, prioritize, decide
Being bandwidth friendly is simple: just send down less data.
Being bandwidth friendly is simple: just sending down less data.
as some user-initiated event which causes the view to update in a meaningful way
for the sake of this presentation, let's define...
Start on View 1
End on View 2
NOT MUCH ROOM TO OPTIMIZE
only 1 possible interaction
strategy: don't mess up
Complex Landing Page
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
non-evil ouija gif
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"
Being bandwidth friendly is about minimizing the payload and using various strategies to postpone, tailor & avoid sending data to end-users
without defining a bandwidth budget, you can't know good from bad
initial load payload limits should be based around product goals & requirements, but a good baseline:
< 500K 💰
< 750K 😀
< 1MB 😐
> 1MB 🤨
on your budgeting because all 30KBs aren't equal:
30KB Photo 💰
30KB Platform Framework 😀
30KB Font 😐
30KB date-formatting lib 🤨
30KB Icon 🙃
get the whole team on-board
💥
(show quick demo)
vs code plugin: "Import Cost"
💥
part 1: don't mess up
it goes a long way™
server configuration that returns zipped content if the browser asks for it
after gzip
before gzip
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!"));
the header
first request
subsequent request
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!"));
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
can you read the code?
not minified
maybeminified
yes
no
minification and optimization tools exist for all asset types:
part 2: optimize
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
.hero {
background: url("./400x267.jpeg");
@media (min-width: 400px) {
background: url("./600x401.jpeg");
}
@media (min-width: 800px) {
background: url("./800x534.jpeg");
}
}
<img
srcset="./800x534.jpeg 800w,
./600x401.jpeg 600w,
./400x267.jpeg 400w"
sizes="(min-width: 800px) 50vw, 100vw"
/>
<picture>
<source
srcset="mdn-logo-wide.png"
media="(min-width: 600px)">
<img src="mdn-logo-narrow.png" alt="MDN">
</picture>
yes you can
April 2018
@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
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(cacheName).then(function(cache) {
return cache.addAll(
[
'/static/main.css',
'/static/main.js',
'/offline.html'
]
);
})
);
});
yeah, pretty much
April 2018
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
[
{
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
allUsers {
id
name
profileUrl
profilePic
}
[
{
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"
}
}
];
such as Apollo are sophisticated enough to know you already have user name and will optimize the request to avoid duplication
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
like these images, for example?
like slides 2-10 for example?
If only there was an event for content entering / exiting the viewport
🤔
💥
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);
const callback = ([entry]) => {
const stub = entry.target;
stub.innerHTML = `<img src="3mb-image.jpg" />`;
};
import Lazy from '@lds/eden-lazy'
<Lazy>
<img src="/path/to/3mb.jpg" alt="big image" />
</Lazy>
or just use
like the currently hidden, view comments pane
{this.state.pane === "comments" && (
<Fetch
url={`/api/comments/${id}`
render={({ data }) =>
data && <CommentsPane {...data} />
}
/>
)}
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);
}
}
like this 3rd-party, rich-text editor
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>