Tree Shaking the Bytes Away

WeAreDevelopers World Congress 2024

Francesco Novy, July 19th 2024

Francesco Novy

www.fnovy.com

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

  1. Generate Dependency Graph (Static Analysis)
  2. 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!

www.fnovy.com

hello@fnovy.com

mydea

Francesco Novy

Tree Shaking the Bytes Away

By Francesco Novy

Tree Shaking the Bytes Away

  • 104