Svelte at Scale: Lessons Learned from 1 Billion Monthly Renders
- Jacob Stordahl Software Engineer @ Stylitics
At Stylitics, we ship Javascript Widgets to some of the largest e-commerce sites in the world. In this talk we'll explore why we choose Svelte to build our products, and what we've learned from maintain one of the most visited Svelte apps in the world.
Transcript
Hello, my name's Jacob Stordahl.
This talk is called Svelte at Scale,
Lessons Learned from 1 Billion Monthly Renders.
It's a bit of a click baity title,
but I really am just hoping to answer
the evergreen question of does Svelte scale
based on the work that I do every day,
full time in my day job.
I would argue 100% yes, Svelte does scale.
And I hope that the things I share today
convince you of that as well.
A bit about myself, I'm a software engineer at Stylitics.
We'll talk a bunch about what I do at Stylitics
and what Stylitics is.
I started learning web development in 2015
while I was in college.
I've always been obsessed with the web
ever since I was a kid.
And after sort of learning the nuts and bolts
of HTML and CSS in 2015,
I've been obsessed with the web
as a platform for software development ever since then.
I picked up Svelte in 2019,
shortly after Svelte 3 was released.
And I've been a huge enthusiast and evangelist
for the language and the framework
that is Svelte ever since then.
A little bit about Stylitics.
We are a fashion and e-commerce technology company.
We provide a complete and tense solution
for e-commerce retailers to create bundles
of their products, most commonly in apparel.
Those would be outfits.
We do work with other product categories as well,
but we aim to provide sort of a complete solution
for some of the biggest retailers in the world
to do this bundling of products at scale.
The backend part of our technology is all Clojure.
We have a huge Clojure culture
and most of our engineering team is as Clojure engineers.
They build our internal tooling and our APIs,
which is what the front ends consume.
Those front ends being all third-party JavaScript
sort of embedded widgets,
different sizes and shapes and functionalities.
And those are all built with Svelte.
So I hope that lends some credence and credibility
to what I'm gonna say today.
A little bit about how we're using Svelte
at Styletics particularly, like I said,
it's all third-party JavaScript.
Our clients download a script tag,
which then makes a class available on the window object
where then they can configure, instantiate,
and start our widget on their page.
This is a really, really good use case for Svelte.
And really we have this sort of large monorepo
that is comprised of a bunch of Svelte code.
And so what does scale mean for us?
So what does scale mean for us?
I back in January, I looked at our monthly averaged page view events.
So anytime the widgets are loaded, they fire a page view event.
And we average about one billion a month.
Some months are a bit slower, more like 800 million.
But on average, we're really close to that 1 billion mark,
which is really, really cool.
And based on what I've been able to find
on the open internet,
in terms of other sites that are using Svelte,
I also showed these numbers to Rich
when we got coffee a while ago,
and I just mentioned this and he seemed to agree.
It's pretty likely that we are the largest
traffic Svelte application in the world
with this number of events.
Also the internal scale is quite large.
We have over 200 Svelte components in our code base.
We build and distribute seven products
using those components as well as obviously
JavaScript code.
And we have four main contributors.
Our team is growing, so there will be even more,
but there's lots of sort of engineering work happening
in this code base day to day.
Yeah.
So the main part of this talk is I wanna share
the sort of lessons that we've learned
building this size of a Svelte application,
shipping it to this many users every month.
I think that we've learned a lot
and I would love to share some of those learnings
with everyone here today.
The first being that Svelte is a language
more than it is a framework.
Certainly it is a framework in its own right,
but I find that thinking of Svelte as its own language
will drive you to build more clear
and concise Svelte applications.
There are lots of differences that Svelte has
even with HTML and JavaScript.
There are some HTML and JavaScript patterns
that are applicable in Svelte,
but there are cases where those HTML
and JavaScript patterns that you know
are actually not the best way to do something in Svelte.
And so I think it's imperative
that if you're going to be working in a large Svelte code
base, that you learn those patterns
and you learn the nuances that come with Svelte
as a language in addition to being really, really solid
on HTML and JavaScript.
Yeah, if you're coming from a different framework,
You might see things like this.
When I joined Stylitics,
there was a lot of this sort of JavaScript template strings
inside of curly braces in the Svelte template.
Whereas the sort of a more correct way to do it in Svelte
is just to use the template brackets.
You know, this is, I believe,
technically still calling a function
to render this string using these variables.
And I think the Svelte way is a little more concise.
At the very least, it's fewer characters
for the compiler to compile.
So there's that, if you care about those tiny details.
Next is component shared global state.
I think component state is pretty cut and dry,
whether you're just using let variable declarations
or even using stores internally inside a component.
I think those patterns are pretty well covered in the docs,
but I think that the ways to approach global
or shared state in your component tree,
there's a lot of different patterns
that you can come up with.
And I wanted to highlight three that we use
both context and stores to accomplish in our application
and the use cases for those.
The first one is that our widgets are really configurable.
Our clients can pass,
I think over 70 different configuration options
to the widget.
And so this is a really, really great use case for context.
These values are not changing that often, if ever.
And so just having this information available
sort of created in context at the top level
of the component tree,
so that it's available to whatever component down the tree
might need some configuration.
It's just available and context makes that
really, really simple.
I would urge any context you write to be well documented.
There's some IDEs that don't know that the context exists.
Personally, I'm a Vim user
and I don't necessarily know what a context is
because it's not being directly imported, right?
We're not using TypeScript
in most of our code base at this point.
So please, yeah, make sure to document those contexts.
So it's easy to find what's available
in your particular component.
The other thing is environment information.
For most products, maybe the environment isn't as important,
but because we're building third-party JavaScript,
getting things like information about the browser
and the user agent, the viewport,
and sort of media settings,
reducing motion, contrast settings,
those sorts of things are all really great fits for context.
Again, they're not changing often,
they're more or less static.
And so just like the configuration,
just having those available in the context
so that if a component needs it,
it can just pull it in without having to prop drill
or deal with like importing ES modules
or anything like that.
It's a really great fit.
Next is stores.
You've probably used stores.
They're kind of a really, really great feature of Svelte.
And I wanted to highlight some of the ways
that we use stores personally.
There are obviously tons of different use cases for stores.
These are just the kind of patterns that we've developed
I wanted to share.
First being building these tiny state machines
for subsets of your application that are more or less isolated.
I'll give an example.
I can't show it because it's an unreleased product,
but I built recently a, when you see an outfit,
you can click on an item and it will open sort of like a modal
in front of the outfit that shows the item you clicked on,
and then you can carousel through the rest of those items.
That whole modal is very self-contained,
And so I used a store to create a tiny state machine
that controls the state of that entire modal.
And the cool thing about this is that,
as my second point here,
it makes it really simple to build stateful components
that need to be sort of mutated or controlled
from many different places within the component tree.
Because this modal was completely derived at state
from this store, I could import that store anywhere
in the component tree that I wanted to activate the modal
and just update the store in place in a click handler
or wherever I needed to.
And so it made adding interactions with this modal
across the application way, way simpler.
And just being able to, you know,
if we were using TypeScript as well,
having a type for that state machine
would be incredibly helpful in the editor
and make sort of interweaving these kinds of UIs together
a lot simpler.
And the other thing is really anything
that's highly interactive or reactive,
anything that is going to be updating frequently.
Stores are really fast in that their implementation
is so simple and I guess close to the metal
if JavaScript is the metal.
You know, they're very simple in their implementation.
They're more a spec than anything else
and they can be used to track and manipulate
lots of different interactions really, really fast.
So we use a store to track aspects of keyboard navigation,
for example, I will say that you should probably not
deviate from what the platform advises
in terms of keyboard navigation,
use native tab order and tab indexes and stuff like that.
Our UI is a little outside of what the platform allows.
And so we take on the responsibility
of highly managing the keyboard navigation.
Most, if you don't know what you're doing,
please don't do that.
But we think that we do, and we have,
I've tested and QA'd this implementation heavily.
and we believe that it makes our product more accessible.
And stores are a huge part of that.
We're able to not only know,
but also manipulate what is happening
when a user interacts with their keyboard
or a mouse or anything like that.
And last, in terms of this global shared state
is a combination of the two.
I think a pattern that I don't see probably as often
as I would like, because it is really, really powerful,
is passing stores into a context.
So the real kind of light bulb moment
was a pull request I read from my colleague, Jan,
where we have recently re-implemented our carousel
to make it a bit better, make it infinite scrolling,
add some extra features to it.
And what we were doing was we had a slot
that you would render the sort of the cards
of the carousel in, which makes it really composable.
It's a really nice component architecture
to be able to kind of treat a carousel as a wrapper
and just give it components as cards
through like an each block or something.
But we found that what we were doing
was all of the sort of metadata about the carousel
that we wanted the children to be knowledgeable of
is through props.
And it was really, really messy.
It was really, lots of children would, you know,
also be used outside of the carousel.
And so there was just lots of extra props
or children not having props that they needed.
And so what we did, what Yon did,
which I really, really like,
and we plan on implementing this pattern
throughout the code base in different places,
is creating a store that encapsulates that metadata,
even, you know, we have like callback functions
we can call from within the carousel into a store
and then pass that to context so that it's available
to any children of the carousel.
And so all of the children get all that metadata,
but we don't have to deal with prop drilling.
It keeps the code a lot cleaner,
a lot easier to understand.
Again, if we were using TypeScript,
we would write a type for that,
and then you would have really good IDE,
sort of Intel sense of what is available
in this context that is in your component.
If you haven't tried this pattern out,
I really recommend it.
It's especially if you're building
any sort of component library or design system,
this can be a really, really powerful full pattern
to make your components more composable.
Testing.
We, for some context,
we, you know, five months ago,
we had very few unit tests
and had a ton of end-to-end Cypress tests.
Cypress is a great tool, but I think, you know,
we were using it too much for too many things.
It resulted in long build times for the tests to run
in CI and locally.
And so ultimately we have been making a migration
to VTest and Testing Library.
The combination of these tools, they're really great.
They play really well together.
And we've been able to write, you know,
300 plus tests with VTest and testing library
in just two months.
So it's a really, really quick workflow.
I will say some caveats and things to look out for
when testing complex components,
like writing tests for them can be equally as complex
as the components themselves.
You, if you're starting somewhere new,
rather than like what we're doing is migrating.
A lot of our components are like super complex
and that makes them way harder to test.
There's lots of components
that have no default value for props.
There's lots of components that consume
like a large number of contexts
that are also don't have default values in the component.
And so you end up having to mock a ton of data
for just testing something as simple
as the component rendering in the DOM.
And so if you're building something new,
try to give your props default values,
try to make it so that you don't have to mock
the whole component essentially
just to test some small part of it.
If the component is hard to test,
probably we could benefit from a refactor
maybe into multiple components.
I don't really subscribe to like really strict
test-driven development at all.
But I think when building Svelte components,
especially that will be shared and consumed
by an unknown number of applications or users,
try to keep them simple,
try to keep them easily testable
and refactor if it makes sense.
Okay, I gotta zoom out a little bit to talk about actions.
I think actions are really the unsung hero of Svelte.
They provide a really powerful abstraction,
especially in a large code base like ours.
You can sort of package up your business logic,
specifically logic that relates to the DOM directly
and create almost, yeah, like a logic component,
if that makes sense.
It creates sort of this functional programming model
for DOM side effects is how I think of it.
Others will probably disagree with that.
But I have a little example of something
that we're looking at using actions for
to clean up our code base.
I think this is a great example of what I'm talking about.
We provide user engagement analytics for our clients.
And so we track things like items that are clicked,
items that are viewed,
outfits that are interacted with in a number of ways.
And so our previous approach was just to kind of copy
paste some existing code into the component
that needs to be tracked and then apply it,
like bind a variable to the HTML that needs to be tracked
for a click or whatever,
and pass those things to the tracking code.
But this is essentially what actions do.
We obviously get the node and the params,
params being the kicker
because this kind of keeps it really flexible
in what you can do with an action.
And so because these tracking events relate
so closely to DOM,
now we can just sort of package this thing up
and create an action
and sort of silo all of that business logic away
into a place that is like a single source of truth
and a very testable unit of code.
So this is not really what we do.
This is just a simplified version of what I'm talking about.
You know, we get a callback pass through params
that we wanna track the click.
We can add the event listener for that.
Now this item is tracked per se.
Then we can also, you know,
do things like intersection observers
for visibility tracking.
If an item is in the viewport, we want to send an event
that says the user saw it.
And so all of these different sort of logic
can be encapsulated in this action
and sort of packaged up for consumption
throughout your application.
Keeping the component code way cleaner,
way easier to reason about, again, way more testable.
So yeah, I highly recommend you lean into actions
if you're in any sort of large scale code base.
There's tons of different use cases for them
and they can keep your code clear and clean
and way easier to maintain,
especially for the person who is gonna be maintaining it
two years from now.
Yeah, highly recommend this.
Okay, takeaways.
Svelte's DSL is deceptively flexible.
These patterns that you can choose
have reasons for choosing them and document them.
Again, there's a ton of different ways to approach different problems in Svelte.
And first understanding the language and then making your own decisions for what patterns
make sense for your application is really, really important.
And then obviously documenting them because it is more flexible than you might assume
with it not being true JavaScript or whatever.
Think long-term.
Obviously, within reason to velocity, we don't want to jeopardize shipping a product because
we're trying to plan for a future that may not come.
But try to think a reasonable amount in the future.
If you keep your components simple, testable, composable in your components, they'll be
be much easier to scale and way more reliable to use
in your application.
I hope that this information was helpful.
I hope that the things I said are clear.
And yeah, if you have any questions, ping me on Twitter.
And yeah, I would love to chat more about Svelte, as always.
Thank you so much.