Network, Bundle, and Memory Tradeoffs
JavaScript performance is affected by more than the code that runs after loading.
Network requests, bundle size, and memory usage can all shape the user experience.
Network Performance Basics
Every request has costs.
The browser may need to:
- resolve a domain
- connect to a server
- negotiate security
- send the request
- wait for the response
- download the response body
Modern protocols are efficient, but unnecessary requests still add overhead.
Reduce Unnecessary Requests
Avoid fetching the same data repeatedly when it has not changed.
let cachedSettings;
async function getSettings() {
if (cachedSettings) {
return cachedSettings;
}
const response = await fetch("/api/settings");
cachedSettings = await response.json();
return cachedSettings;
}This simple cache may be enough for data that rarely changes.
For changing data, use expiration or invalidation.
Request Only What You Need
Large responses take longer to download and parse.
If an API supports query options, request only needed data.
const params = new URLSearchParams({
limit: "20",
fields: "id,name,avatarUrl",
});
const response = await fetch(`/api/users?${params}`);Smaller responses can improve both network and JavaScript parsing time.
Bundle Size
A JavaScript bundle is the code sent to the browser.
Large bundles can slow down:
- download
- parsing
- compilation
- startup execution
Ways to reduce bundle cost include:
- removing unused dependencies
- using smaller libraries
- code splitting
- lazy loading rare features
- avoiding duplicate packages
- shipping production builds
Tree Shaking
Tree shaking is a build tool optimization that removes unused exports.
import { formatDate } from "./formatters.js";If the build tool can prove that other exports are unused, it may remove them from the final bundle.
Tree shaking works best with ES modules and side-effect-free code.
Avoid Heavy Dependencies for Small Jobs
Before adding a package, ask what it costs.
const formatter = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
});
console.log(formatter.format(new Date()));Built-in browser APIs can sometimes replace a dependency.
Do not avoid packages blindly.
Use them when they provide real value.
Memory and Speed Tradeoffs
Some optimizations use more memory to save time.
Examples:
- caching API responses
- memoizing function results
- preloading data
- building lookup maps
- keeping hidden UI already rendered
These can be good choices.
They can also increase memory usage and make leaks more damaging.
Building Lookup Maps
If you repeatedly search an array by id, a Map can help.
const productsById = new Map();
for (const product of products) {
productsById.set(product.id, product);
}
function getProduct(id) {
return productsById.get(id);
}This uses extra memory, but it avoids repeated full-array scans.
Use this when lookups are frequent enough to justify the setup cost.
Avoid Premature Preloading
Preloading can make later interactions faster.
But loading too much early can hurt startup.
link.rel = "prefetch";
link.href = "/reports.js";
document.head.append(link);Prefetch rare features only when the browser and network are likely to have spare capacity.
Common Mistakes
- Adding a large dependency for a small utility.
- Loading all routes or screens during startup.
- Caching data forever without memory or freshness rules.
- Downloading large API responses and using only a few fields.
- Improving CPU time while making startup network cost worse.
Summary
Performance involves tradeoffs between network, CPU, memory, and user experience.
Reduce unnecessary downloads, keep bundles focused, request only useful data, and use memory-based optimizations only when the speed benefit is worth it.