Bundle Size matters - this is something we SDK engineers at Sentry are acutely aware of. In an ideal world, you'd get all the functionality you want with no additional bundle size - oh, wouldn't that be nice? Sadly, in reality any feature we add to the JavaScript SDK results in additional bundle size for the SDK - there is always a trade off to be made.
With Session Replay, this is especially challenging. Session Replay allows you to capture what's going on in a users' browsers, which can help developers debug errors or other problems the user is experiencing. While this can be incredibly helpful, there is also a considerable amount of JavaScript code required to actually make this possible - thus leading to an increased bundle size.
In version 7.73.0 of the JavaScript SDKs, we updated the underlying rrweb package from v1 to v2. While this brought a host of improvements, it also came with a considerable increase in bundle size. This tipped us over the edge to declare a bundle size emergency, and focus on bringing the additional size Session Replay adds to the SDK down as much as possible.
We're very happy to say that our efforts have been successful, and we managed to reduce the minified & gzipped bundle size compared to the rrweb 2.0 baseline by 23% (~19 KB), and by up to 35% (~29 KB) with maximum tree shaking configuration enabled.
Steps we took to reduce bundle size
In order to achieve these bundle size improvements, we took a couple of steps ranging from removing unused code to build time configuration and improved tree shaking:
Made it possible to remove iframe & shadow DOM support via a build-time flag
Removed canvas recording support by default (users can opt-in via a config option, support is coming)
Removed unused code from our rrweb fork
Removed unused code in Session Replay itself
Made it possible to remove the included compression worker in favor of hosting it yourself
Moved to a different compression library with a smaller footprint
Primer: rrweb
rrweb is the underlying tool we use to make the recordings for Session Replay. While we try to contribute to the main rrweb repository as much as possible, there are some changes that are very specific to our needs at Sentry, which is why we also maintain a forked version of rrweb with some custom changes.
Primer: Tree Shaking
Tree shaking allows a JavaScript bundler to remove unused code from the final bundle. If you're not familiar with how it works and the advantages tree shaking brings, you can learn more about it in our docs.
Made it possible to remove iframe & shadow DOM support via a build-time flag
While rrweb allows you to capture more or less everything that happens on your page, some of the things it can capture may not be necessary for some users. For these cases, we now allow users to manually remove certain parts of the rrweb codebase they may not need at build time, reducing the bundle size.
In getsentry/sentry-javascript#9274 & getsentry/rrweb#114 we implemented the ground work to allow for tree shaking iframe and shadow DOM recordings. This means that if, for example, you don't have any iframes on your page, you can safely opt-in to remove this code from your application.
In getsentry/sentry-javascript-bundler-plugins#428 we implemented an easy way to implement these optimizations in your app. If you are using one of our bundler plugins:
You can just update to its latest version, and add this configuration to the plugin:
sentryPlugin({
bundleSizeOptimizations: {
excludeDebugStatements: true,
excludeReplayIframe: true,
excludeReplayShadowDom: true,
},
})
This will save you about 5 KB gzipped of bundle size!
How we implemented build-time tree shaking flags
We already had some build-time flags for tree shaking implemented in the JavaScript SDK itself (__SENTRY_DEBUG__
and __SENTRY_TRACING__
). We followed the same structure for rrweb:
// General tree shaking flag example
if (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) {
console.log('log a debug message!')
}
By default, this code will result in log a debug message
! being logged. However, if you replace the __SENTRY_DEBUG__
constant at build time with false
, this will result in the following code:
if (typeof false === 'undefined' || false) {
console.log('log a debug message!')
}
Which bundlers will optimize to the following:
if (false) {
console.log('log a debug message!')
}
And in turn, since the code inside of if (false)
will definitely never be called, it will be completely tree shaken away.
For rrweb, we used the same approach to allow you to remove certain recording managers:
In order to avoid touching all the parts of the code that may use a manager, we added new dummy managers following the same interface but doing nothing:
interface ShadowDomManagerInterface {
init(): void
addShadowRoot(shadowRoot: ShadowRoot, doc: Document): void
observeAttachShadow(iframeElement: HTMLIFrameElement): void
reset(): void
}
class ShadowDomManagerNoop implements ShadowDomManagerInterface {
public init() {}
public addShadowRoot() {}
public observeAttachShadow() {}
public reset() {}
}
Now, in the place where the ShadowDomManager
is usually initialized, we can do the following:
const shadowDomManager =
typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' && __RRWEB_EXCLUDE_SHADOW_DOM__
? new ShadowDomManagerNoop()
: new ShadowDomManager()
This means that by default, the regular ShadowDomManager
is used. However, if you replace __RRWEB_EXCLUDE_SHADOW_DOM__
at build time with true
, the ShadowDomManagerNoop
will be used, and the ShadowDomManager
will thus be tree shaken away.
Removed canvas recording support by default
Since we currently do not support replaying captured canvas elements, and because the canvas capturing code makes up a considerable amount of the rrweb codebase, we decided to remove this code by default from our rrweb fork, and instead allow you to opt-in to use this by passing a canvas manager into the rrweb record()
function.
We implemented this in getsentry/rrweb#122, where we started to export a new getCanvasManager
function, as well as accepting such a function in the record()
method. With this, we can successfully tree-shake the unused canvas manager out, leading to smaller bundle size by default, unless users manually import & pass the getCanvasManager
function.
Once we fully support capturing & replaying canvas elements in Session Replay (coming soon), we will add a configuration option to new Replay()
to opt-in to canvas recording.
Removed unused code from rrweb
Another step we took to reduce bundle size was to remove & streamline some code in our rrweb fork. rrweb can be configured in a lot of different ways and is very flexible. However, due to its flexibility, a lot of the code is not tree shakeable, because it depends on runtime configuration.
For example, consider code like this:
import { large, small } from './my-code'
function doSomething(useLarge) {
return useLarge ? large : small
}
In this code snippet, even if we know we only ever call this as doSomething(false)
, it is impossible to tree shake the large
code away, because statically we cannot know at build time that useLarge
will always be false
.
Because of this, we ended up fully removing certain parts of rrweb from our fork:
-
hooks
related code getsentry/rrweb#126 -
plugins
related code getsentry/rrweb#123 Remove some functions on
record
that we don't need getsentry/rrweb#113
In addition, we also made some general small improvements which we also contributed upstream to the main rrweb repository:Avoid unnecessary cloning of objects or arrays getsentry/rrweb#125
Avoid cloning events to add timestamp getsentry/rrweb#124
Removed unused code in Session Replay
In addition to rrweb, we also identified & removed some unused code in Session Replay itself:
- Clean up some logs and internal checks getsentry/sentry-javascript#9392, getsentry/sentry-javascript#9391
- Remove unused function getsentry/sentry-javascript#9393
Updated library used for compression
We used to compress replay payloads with pako, which, while it worked well enough, turned out to be a rather large (bundle-size wise) library for compression. We switched over to use fflate in getsentry/sentry-javascript#9436 instead, which reduced bundle size by a few KB.
Made it possible to host compression worker
We use a web worker to compress Session Replay recording data. This helps to send less data over the network, and reduces the performance overhead for users of the SDK. However, the code for the compression worker makes up about 10 KB gzipped of our bundle size - a considerable amount!
Additionally, since we have to load the worker from an inlined string due to CORS restrictions, the included worker does not work for certain environments, because it requires a more lax CSP setting which some applications cannot comply with.
In order to both satisfy stricter CSP environments, as well as allowing to optimize the bundle size of the SDK, we added a way to tree shake the included compression worker, and instead provide a URL to a self-hosted web worker.
Implemented in getsentry/sentry-javascript#9409, we added an example web worker that users can host on their own server, and then pass in a custom workerUrl
to new Replay({})
. With this setup, users save 10 KB gzipped of their bundle size, and can serve the worker as a separate asset that can be cached independently.