9 minute read
And as a startup, Split also had its list of priorities, including being able to iterate quickly, keep costs low, and coordinate between a team of developers that spoke multiple languages (and we don’t just mean programming languages!).
- Designing a consistent architecture to be implemented in all languages and a language-agnostic specification to follow through, as closely as it made sense given platform differences.
- Implementing a
`LocalStorage`caching option, to support persistent browser caching.
- Support for a
`localhost`mode, so people can easily write tests for features that use flags.
- Performing feature flag evaluations at the SDK level to keep end-user data secure and enable targeting by user attributes without requiring PII be sent to Split.
- Informing the user of invalid inputs or possible misconfiguration (e.g. if multiple instances of the same config are created) to help development teams diagnose issues faster.
- Adding streaming support – a feature that ensures that changes made in the backend are reflected in the browser in near real-time. How neat is it to propagate flag changes incredibly fast?
The performance and resilience of our SDK were always top of mind, and throughout the past few years, we’ve worked on many improvements. But as we continued to provide additional features that met the needs and requests of customers, we realized that the SDK was becoming increasingly unwieldy. This contributed to higher page load times, negatively impacting user experience and subsequently SEO ranking.
So we set out to analyze and make changes to decrease the SDK size while maintaining functionality.
Step 1: Measuring and chipping away at major offenders
We started by measuring different builds and combinations. We analyzed the output with tools like Webpack bundle analyzer and tried to identify the biggest modules we could cut off.
We made a number of adjustments:
- We removed our HTTP library Axios, which served us well for quite some time as a strong isomorphic solution, in favor of native fetch for browsers and node-fetch on the server-side.
- We tweaked our build to the detail and removed our regenerator-runtime dependency at the expense of not using some of the new features like async/await in our source code.
- We validated no splits still active with our oldest hashing algorithm so we cleaned it up too.
- We started looking for verbose implementations to improve by refactoring the code.
- We even looked at how the syntax that is not implemented in our range of supported browsers is finally transpiled and stopped using the ones that consumed unnecessary bits. Or moved a few things around to favor minification of variable names.
These efforts spanned a few months and in between we’ve added features like streaming support (increasing the amount of code), but from the peak to today we’re at around 58% of the original size!
But while we could continue to make incremental changes, the impact was going to be small. We had to face the fact that the SDK had a lot of functionality and the biggest chunk left was just source code.
Step 2: Modularizing the SDK
We had explored this approach before with our Golang implementations, where we reuse different modules from both the Go SDK and the Split Synchronizer, as well as even some internal microservices. This approach has been working great!
Analyzing our current situation, we needed to determine how to keep our APIs from unexpected changes as well as to more securely integrate them from consumer libraries by different developers. We needed support for types. (There are other advantages to having types, like helping avoid undetectable issues of a big and complex dynamic typing ecosystem of modules and utilities.)
We chose to use Typescript, a language we already knew and for which we already had type definitions to use as a starting point. Besides, being statically typed, there’s no impact in our bundles 😊
Once we had a clear path forward, we migrated all functionality into different modules aimed to be consumed from different implementations and enabled tree shaking (removal of dead code) as one of the main drivers. Storage implementations, synchronization mechanisms, input validation utilities, logging functionality—everything was moved and tweaked to be reusable modules.
We moved all tests and this time around we chose Jest as the testing framework of choice. It’s a fast, extensible, robust testing framework that comes with assertions and anything you need and it’s also great to test code in a platform-agnostic environment.
Step 3: Creating a pluggable Web SDK
With the regular SDK, if you want to use the browser LocalStorage as a cache, given you always have the code available, all you need is to pass a string constant and an optional prefix if desired.
Code language: Go (go)
While this is a commodity, if you don’t use that cache, you still have it’s implementation code in your bundle.
With the new SDK, even though the pluggable API is not part of the modules imported by the entry point, you can plug it in a breeze!
Another example are the integrations. Same situation as before, with the regular SDK we have the code always available so we can set them up with a constant:
Code language: Scala (scala)
In the new SDK, these are also optional:
While we only support two Google Analytics implementations (i.e. sending Split data GA and the other way around) as a first class SDK to SDK integration at this time, we’ve implemented those on top of a generic framework so please let us know if there’s a particular one you want to see!
The logger is also part of the first pluggable module the SDK supports as it allows us to extract the bulk of the log message strings, yielding great results when it comes to reducing size of the code. And we’ll keep “plugging” away at making additional modules pluggable for our customers to leverage in the future.
Step 4: Measuring the impact of our changes
When comparing the full blown SDK with full functionality versus the new Browser SDK in its now slimmer and trimmer state, we have managed to reduce up to 45% of the impact in the bundles! And If we compare the JS SDK in its heaviest version to the minimal Browser SDK, there’s more than 100% improvement ! 🤯
And the cool thing is that you don’t need to choose between the minimal and full versions. You can start small and add what you want on top of it. If you’re not sure, you can start with the full set of features and then if you find yourself not using most of the features, migration will be easy enough.
In exchange you get a full blown API supporting all features, without any extra hassle to ensure you’ve configured what you want to use as well as support for IE11 out-of-the-box.
And while the spring cleaning that we did with our SDK happened to coincide with the start of spring, we’ll continue to evaluate additional updates, including more pluggable modules, support for generic integrations and other ideas.
If you’re working on a new Web application or improving your page load times, try out our new Web SDK!
- Create a Single Page Application with React and React Router
- Controlled Rollout with React Native and Feature Flags
- Build a Simple REST API with Node and Postgres