Tree Shaking the Bytes Away
WeAreDevelopers World Congress 2024
Francesco Novy, July 19th 2024
Francesco Novy
hello@fnovy.com
mydea
- Located in Vienna, Austria
- 10+ years of working with JS
- At Sentry since 2022
- Working on the JavaScript SDKs
Overview
- Sentry & SDKs
- What is Tree Shaking?
- How do Bundlers work?
- Understanding Static Analysis
- How to Test Tree Shaking
- How to Write Tree Shakeable Code
What does Sentry do?
- Error Monitoring
- Performance Monitoring
- Session Replay
- Profiling
Sentry JavaScript SDKs
- Sentry provides a variety of SDKs
- 20+ JavaScript SDKs (React, Next.js, Node, ...)
- SDKs are included by other developers into their app
- Not all SDK features are used by every developer
How can we avoid to ship code to users for features they are not even using?
With Tree Shaking!
What is Tree Shaking?
-
Tree Shaking describes the ability to automatically remove unused code from your build.
- When code is written in a tree-shakeable way, bundlers like Webpack or Vite can optimize your application based on what is actually used.
What is Tree Shaking?
Bundlers
- Webpack
- Vite
- esbuild
- Rollup
- ... any many more!
Why Use Bundlers?
- Mostly relevant for browser, but also f.e. serverless
- Relevant when you use import or require
- You can use import natively in the browser today!
- ... but the browser will always load the full files.
Using import without Bundler
<html>
<body>
<script src="index.js" type="module"></script>
</body>
</html>
import { add } from './math.js';
console.log(add(1, 2));
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
index.html
index.js
math.js
Pros:
Pros & Cons of Using Unbundled Code
Cons:
- No build step necessary
- Potentially a lot of HTTP requests
- Not possible to tree shake unused code
What do Bundlers do?
- Combine all used code into a single file (usually)
- Tree shake unused code away
- Bundlers != Transpilers (Babel, Typescript, ...)
- ... but often Bundlers allow to transpile via plugins.
How do bundlers tree shake?
With Static Analysis!
Bundling Steps
- Generate Dependency Graph (Static Analysis)
- Bundle the required elements into a single file
Static Analysis
- Static analysis happens at build time, based on your code.
- It cannot take runtime configuration into account!
- Only things that are considered “static” can be tree shaken.
Dependency Graph
Dependency Graph
- getSizes
- getDistribution
- calculateSum
- calculatePercentage
- add
- multiply
Static vs. Dynamic Code
Only static code can be tree shaken!
Dynamic Code Example
import { add, subtract } from './math.js';
export function addOrSubtract(shouldAdd) {
if (shouldAdd) {
console.log(add(1, 2));
} else {
console.log(subtract(1, 2));
}
}
Static Code Example
import { add, subtract } from './math.js';
// const is static!
const SHOULD_ADD = true;
export function addOrSubtract() {
if (SHOULD_ADD) {
console.log(add(1, 2));
} else {
console.log(subtract(1, 2));
}
}
import { add } from './math.js';
export function addOrSubtract() {
console.log(add(1, 2));
}
bundled to
Static Code Example 2
const IS_DEBUG = false;
function runCalculation() {
if (IS_DEBUG) {
console.log('calculation started');
}
const res = 1 + 2;
return expensiveCalculation(res);
}
function runCalculation() {
console.log('calculation started');
return expensiveCalculation(3);
}
IS_DEBUG=true
function runCalculation() {
return expensiveCalculation(3);
}
IS_DEBUG=false
How to Test Tree Shaking?
size-limit
npx size-limit
module.exports = [
{
name: '@sentry/browser (incl. Tracing)',
path: 'packages/browser/build/npm/esm/index.js',
import: '{ init, browserTracingIntegration }',
gzip: true,
},
{
name: '@sentry/browser',
path: 'packages/browser/build/npm/esm/index.js',
import: '{ init }',
gzip: true,
},
];
.size-limit.js
size-limit Results
Writing Tree Shakeable Code
- Using composition
- Using static build-time flags
Composition vs. Options
Composition favors tree shaking!
Composition vs. Options
import { getUser, getParent } from './user.js';
export function printName(userId, printParentName) {
if (printParentName) {
console.log(getParent(userId).name);
} else {
console.log(getUser(userId).name);
}
}
import { getUser, getParent } from './user.js';
export function printName(user) {
console.log(user.name);
}
printName(getUser(userId));
printName(getParent(userId));
Using options - not tree shakeable 🚫
Using composition - tree shakeable ✅
Composition:
Real World Example
// SDK
import {
CanvasManager
} from './canvas-manager';
export function record(options) {
if (options.recordCanvas) {
new CanvasManager();
}
}
// Application
import { record } from 'sdk';
record({ recordCanvas: false });
Using options - not tree shakeable 🚫
Composition:
Real World Example
// SDK
import {
CanvasManager
} from './canvas-manager';
export function getCanvasManager() {
return new CanvasManager();
}
export function record(options) {
if (options.getCanvasManager) {
options.getCanvasManager();
}
}
Using composition - tree shakeable ✅
import {
record
} from 'sdk';
record({ getCanvasManager: undefined });
import {
record,
getCanvasManager
} from 'sdk';
record({ getCanvasManager });
Without using Canvas
With Canvas
Composition vs. Options
- Composition brings tradeoffs in DX
- Decide case-by-case which pattern fits the best
- Not just relevant for SDKs! Also applies to in-app code splitting, ...
Static Build-Time Flags
Leverage static code to optimize bundle size
Static Build-Time Flags:
Real World Example
const IS_DEBUG = __SENTRY_DEBUG__;
function doSomething() {
if (IS_DEBUG) {
console.log("Log some debug info here!");
}
}
Static Build-Time Flags:
Real World Example
const webpack = require("webpack");
module.exports = {
// ... other options
plugins: [
new webpack.DefinePlugin({
__SENTRY_DEBUG__: !!process.env.DEBUG,
}),
],
};
import {
replaceCodePlugin
} from "vite-plugin-replace";
module.exports = mergeConfig(config, {
plugins: [
replaceCodePlugin({
replacements: [
{
from: "__SENTRY_DEBUG__",
to: !!process.env.DEBUG,
},
],
}),
],
});
Webpack
Vite
Static Build-Time Flags:
Real World Example
const IS_DEBUG = __SENTRY_DEBUG__;
function doSomething() {
if (IS_DEBUG) {
console.log("Log some debug info here!");
}
}
const IS_DEBUG = false;
function doSomething() {
if (IS_DEBUG) {
console.log("Log some debug info here!");
}
}
const IS_DEBUG = false;
function doSomething() {
if (IS_DEBUG) {
console.log("Log some debug info here!");
}
}
function doSomething() {
}
The Sentry SDK is Open Source!
- Everything we do is open source!
- Look at the code, PRs, etc.
- We love feedback!
Thank you!
hello@fnovy.com
mydea
Francesco Novy
Tree Shaking the Bytes Away
By Francesco Novy
Tree Shaking the Bytes Away
- 104