Fri Jan 24 2020
Article size is 9.3 kB - 103 kB and is a 8 min read
So let's start with some gloating.
Recently launched FastComments.com was built in a month.
For a high-level view this includes the marketing site, blog, client-side comment plugin, integration with Stripe, data import/exports, comment moderation, and a basic analytics page. Here's the github activity for the repository (yes there's only one, which I'll get into):
Anyway, enough gloating. As you can tell by the name the product had to be fast. That was a major goal. The second goal was UX.
The main driver for this was during my month-long stint in China I thought how I'd write about my trip. Then I thought about how I'd add comments to my blog. No biggy right, use Disqus? Well based on some reading I decided against that and figured I'd build my own.
I didn't want people to go and comment and have to deal with popups to login to Google, Facebook, etc. In fact I didn't want users to deal with any popups at all, or loading screens, high memory usage, or any of that.
FastComments is light and easy. Those are the goals.
People that have worked with me know I'm a huge fan of typesafety, so I seriously considered Java for this project. I even considered bringing the data store largely into memory and attempting to get comment fetch times into the single milliseconds. An LRU cache at least would have worked, but I decided to hold off on that for now.
Right now - FastComments is a monolith (except for the blog which is in its own repo and uses the same static site generation library I wrote for this site).
That monolith is written in NodeJS. I know, I know. When people hear NodeJS monolith they shudder. But hear me out, the code is very simple, easy to maintain, and fast. That doesn't mean it's not sophisticated - there are a lot of different crons with distributed locks to offload work from the event loop and schedule things.
Probably when I finally learn Vertx or Spring MVC I'll dump Express. Right now just my game backends and simple APIs are in Java, I haven't invested the time into learning a Java web framework yet.
Speaking of crons, here are the crons FastComments uses.
- Daily expiration of exported files on disk
- Daily submission of usage of the product to Stripe.
- Hourly comment notifications (to commenters and site owners).
- Minutely batch exports
- Minutely batch imports
Here's what the code for one of the crons looks like. This is the first one listed above.
The import/export ones probably caught your eye. I've worked with enough enterprise software with terrible imports and exports where you sit there, waiting while some backend ten services away from the web layer is synchronously processing your data. The page won't finish loading until it does, and it'll probably timeout. You know the feeling.
So imports and exports in FastComments are asynchronous. When you upload a file to migrate from another platform we enqueue a job and a cron checks for jobs every minute. You'll get an email when it's done, and this way separate servers can be deployed to handle imports rather than impacting the site if need be.
Importing or Exporting creates a job object like this:
Let's take a step back and talk about the Fast part of FastComments. How fast is it?
It seems comment api calls take around 30-50ms. We make one of these when you load the page, and it's the only API request we make to get all the comments and any other information (like, is this request for a valid tenant, etc). So all authentication, validation, and data fetching happens in those ~30ms. Limiting the number of requests reduces latency. This is currently with no load, so we'll see how things scale. But so far it seems to scale well based on my testing.
This also includes DNS lookups and such which sucks a few milliseconds here and there, as well as the nginx reverse proxy. Also, some middleware is sucking up cpu time because Express has to check whether or not the requests falls into those routes. ExpressJS executes middleware sequentially and right now the order of things is static routes -> API routes. I am considering moving the static routes last, and then using Nginx to cache the static assets.
This project is very IO bound. It does very little on the CPU pretty much everywhere so that's why Node is perfect.
The layers of abstraction has also been kept way down. In fact, I wrote zero API middleware for this project. This leads to some code duplication, but it's very minor and just a couple lines here and there. I really wanted this to perform well at scale without spending a ton of money and I was willing to sacrifice some of my sanity to do it.
Performance also means you have to be very aware of your dependencies at runtime - like when you are fetching data from a source like a database. In FastComments we only ever ask Mongo for what we need (and yes, we're using MongoDB). Projections are used everywhere we fetch data - like SELECT a, b, c FROM in the SQL world - we tell MongooseJS to .select('a b c') everywhere we query objects. This helps scaling feature-wise. Increasing sizes of objects will still increase index size, but it won't add overhead to our existing queries/features.
The main aspect of the product - fetching that comment list and rendering it - is fast even with Mongo because we just make a query like "get me all of the comments for this page id". Mongo's doing very little in this scenario, and in fact I think most of our time is spent in serialization in the JS layer. That is one downside of using Mongoose for this, however it's still very fast.
So, Mongo works fine since we don't have to do many joins and where we do it's in less important areas of the product like admin dashboards and such. Those pages still load very fast, which I'll get into, so I'm not worried.
Client side we use zero frameworks. That's required to keep the file size of the client below 15kb (it is 2.6kb gzipped at time of writing). This isn't very hard, even larger apps like Github don't use client side frameworks anymore. document.querySelector/querySelectorAll works well enough to not need jQuery anymore.
The client library is a mix of raw HTML manipulation and a bunch of optimizations. Nothing fancy really, but that's the idea! You can read the source code here: https://fastcomments.com/js/comment-ui.js
In the admin dashboard (My Account, Analytics, Comment Moderation) we use mostly server-side rendering. EJS plays that role and it does it very well. The syntax integrates well with Webstorm and it's pretty quick. SSR can be tricky with state management but since one goal is to keep the complexity down it results in very maintainable code.
I'm also hoping that people that have to use the admin dashboard - like for moderating comments - appreciate how fast it is.
When I say I use EJS, I mean I really use EJS. Also, since there is very little abstraction I've went with a very flat file structure in this project. I have projects with lots of abstraction, very nested folders etc, so this was really some fresh air.
For example, here's my "views" folder:
Deployment wise, FastComments uses the same Orchestrator that all WinrickLabs projects use. I haven't written about the Orchestrator yet but I will. It doesn't support autoscaling yet but that's coming soon.
There are lots more features coming too so keep an eye out for those :) I think the first one is going to be Avatars since every single competitor has that. I just have to make sure it doesn't slow things down.
Anyways it was really fun and nice to build something that had performance and usability in mind. Surprisingly we don't get enough of that today. I think that's going to change though. People are going to get tired of how slow their software is. Enjoy FastComments.