Since its initial release, we've refactored our JavaScript SDK multiple times, and we've written about how previous improvements reduced execution time from 200ms to 20ms.
Since then, the JavaScript SDK has grown in size as we've added support for new device-mode integrations. It became bulky enough to start impacting load times, so we recently introduced a new, optimized version of the SDK.
Here, I'll detail the improvements made with this refactoring, walk through our team's decision-making process, outline the tradeoffs we considered, and showcase the results of our work.
Key improvements
To optimize the size of the SDK and improve its performance, we focused on three key items:
- Freeing the SDK of all integrations code upon build.
- Clearing technical debt
- Replacing third-party package dependencies
Freeing the SDK of integrations code upon build
Instead of statically importing device-mode integration modules into the core module, the integration modules are now built into independent plugins (scripts) that can be readily loaded on the client-side. Once the load
API of the SDK is called, the necessary destination integrations are identified from the source configuration (pulled from the control plane) and their plugins are asynchronously loaded one after another from the hosted location*. After a timeout, the successfully loaded integration modules are initialized to proceed with the events forwarding.
*The hosted location defaults to RudderStack's CDN. In the case of a custom hosted location, this can be overridden via the 'destSDKBaseURL'
option in the 'load
' call. Additionally, SDK determines this URL based on the script tag that adds the SDK on the website (provided the file name is still "rudder-analytics.min.js"
).
Clearing technical debt
We removed as much bloat from the SDK as possible. This included dead, redundant, and deprecated code along with deprecated auto-track functionality.
Replacing third-party package dependencies
Wherever possible, we replaced third-party package dependencies with lighter ones. A few cases required custom implementations in order to achieve the results we were looking for.
Why did we decide on this approach?
By design, all the device-mode integrations are independent of each other, so it didn't make sense to bind everything together as a single piece. Moreover, because each customer will only connect a subset of device-mode integrations to their JS/web source, loading only the necessary integrations on their site is the ideal scenario. These improvements also involved minimal changes to our SDK and processes when compared to other alternatives.
One alternative we considered was to dynamically build the SDK with necessary integrations when the request is made to https://cdn.rudderlabs.com/v1.1/rudder-analytics.js/<write key>
. Using this approach, the device-mode integrations are packaged with the core SDK and delivered based on the write key provided in the URL.
We saw a few disadvantages to this approach:
- CDN costs would increase because we would have to cache a different version of the SDK for every write key
- We wouldn't be able to take advantage of browser caching across various websites the user visits
- Migrating existing users would be challenging
What tradeoffs did we have to make?
Fortunately, this refactoring didn't involve any major tradeoffs, but there are two worth noting:
- CDN Costs: Hosting all of the individual device-mode integration SDKs means increased CDN costs. Luckily, the additional cost is not a significant burden.
- Migration costs: To make migrating to v1.1 worthwhile for our customers, we knew we needed to (1) introduce significant performance improvements over v1, and (2) make migrating as easy as possible. We were able to introduce significant improvements, which I'll highlight below, and we worked to make migration as painless as possible. In most cases, migration is complete in a few simple steps, which we documented in a migration guide to help customers with all their deployment scenarios.
Problems we had to solve
In v1, all the integrations were exported from their module as the default type. We had to convert all of them to named exports for them to be dynamically loaded. See the below example:
Default type
import Amplitude from "./browser";
export default Amplitude;
Named export
import Amplitude from "./browser";
export { Amplitude };
Additionally, we had to write a script to build all the individual integrations in one go. This is what allows us to deploy the integrations along with the core SDK.
Results of the refactoring
Our new SDK is lighter and faster than the previous version. To put it into numbers:
- We reduced the SDK size by 70%. (114 KB to 34 KB)
- SDK download times are 80% faster (9.44 ms to 1.96ms)
- Script evaluation times are 28% faster (86 ms to 63 ms)
Check out the PR for the refactoring on Github.
Top comments (0)