Skip to main content

Paul Armstrong’s profile picturePaul Armstrong

Lessons learned: how I’d rebuild a social web app like Twitter today

I’ve been thinking back to my time as a core member of the original team that built the Twitter web app. I learned a great many lessons there and thinking through them: How would I rebuild something similar to the Twitter web app today, like a Mastodon or Activity Pub web client?

No React Native for Web

We used React Native for Web (RNW) and I have been bullish on its benefits for years. It served me – and us — very well. In fact, its creator was our team’s tech lead for the first few years until he left to join the React core team. While we got a lot out of RNW, we didn’t get any benefit out of the core selling point: cross-platform components. The native apps are highly optimized, do not, and will not use React Native. Without going into too much detail, I’ll just say it is not a battle to even try to fight.

Just as well, web developers tended to have a lot of issues with RNW. Not directly, just that it wasn’t very ergonomic and took too much time to understand. While RNW (and React Native, for that matter) are moving to better direct web compatibility by accepting more web-native styles and props like aria-*= (instead of accessiblity*=), it’s still a bit too much of a shift and frankly the library is falling behind.

For the most part now, all RNW is getting the team now is default styling, automatic LTR/RTL text direction swapping, and some performance help under the hood. None of which aren’t easily handled otherwise.

And while RNW is not a hit on performance directly, pseudo-selectors and interactivity must be handled through React hooks, even just for simple style changes on hover or focus. This does actually translate to an interactivity performance issue.

Solid-js or another non-virtual DOM framework

Performance of Twitter web was absolutely critical when I was working on it. In fact, performance was so critical that we called with “Twitter Lite” (before the Android app was released) because its target market was those that needed to load and use Twitter as fast as possible.

While Twitter and similar Fediverse applications are highly interactive, modifying the DOM mostly happens for virtual scrolling and navigation between URLs. There are a lot of little visual fluff additions, like the way favoriting and retweet counts update in semi-real time. One of the difficult problems we had with those was ensuring only the portion of the UI that needed to update was updating.

With Solid-js, we can remove the virtual DOM and rendering tree to focus solely on small portions of data that update and re-render only what matters without large cascading effects.

Preact recently got signals, similar to Solid, which makes it an interesting alternative middle-ground candidate between React and Solid. If it turned out that the team did not feel comfortable enough to push the envelope that far, Preact may be a suitable choice as well!

Just as well, server components look like a start to changing React into a similar direction, but we’ve been waiting two years (or more) for them and they’re still not quite ready. And on top of all of this, I’m still proposing to move away from React (and I’m not going to get into why we are not jumping on the Next.js bandwagon – that may be a conversation for another day).

Use Tailwind CSS, not CSS-in-JS

Without React Native for Web, a CSS/styling solution would be necessary. After trying many of the options available, I feel confident in proposing Tailwind CSS for almost all new projects.

We would be able to redefine and restrict the color, spacing, and sizing tokens down to exactly what the design team has defined. It is also possible to do most of the heavy-lifting on theming from within the Tailwind configuration.

Most CSS-in-JS solutions do not create separate CSS assets that are cached in the CDN. All of the styles are delivered within the JavaScript bundles, causing extra network, evaluation, and runtime contention within the application. For our performance-first approach, it is imperative that we make gains here.

Bonus: I would start using client hints to determine what theme to serve to users on page load to avoid the current flash of super bright loading screen.

Use Vite or TurboPack

Webpack is showing its age. Vite has shown it is a fast and solid alternative; TurboPack claims to be the successor of Webpack. Given that TurboPack isn’t widely available or tested for non-Next.js applications, I would likely end up choosing Vite, but still look for a path to swap for TurboPack and test as soon as possible.

Webpack configurations at scale become unruly beasts. There were at most a handful of engineers at Twitter (more likely one or two) that were confident in making changes to the configuration. Vite looks like it could simplify that a bit, but it would still be top of mind for me to ensure that the configurations were easy to follow and well-documented.

Split into multiple applications with module federation

Twitter is a unique application – in that it could actually be a number of separate applications that have core shared modules. One thing that we struggled with was moving fast on really small parts of the application, eg, “Settings”. Because everything in the application was interconnected and controlled by a single Node.js process, this made reviews, CI, and deploys slow. We could have, however, split things into multiple applications. For example:

  • Post permalinks
  • Home timeline
  • Profiles
  • Direct messages
  • Settings
  • New user onboarding

These applications then need a number of core shared modules, like the layout, user information, Tweet rendering components, etc.

All of this could be handled with Module Federation in either Webpack or Vite. Module Federation is a newer concept that could handle exactly what I would want: separate applications that reference each others’ shared code to avoid duplication, reduce footprints, allow faster builds and deploys, and much more.

User impact

Module federation presumably would have a net benefit to end users as well. Bundling and separation of code becomes a bit more logical, making shared code more highly available between logged-in vs logged-out users, while also preventing code getting dropped into a shared bundle where it won’t be used. Essentially: we would deliver only the JavaScript necessary – reducing payloads, byte transfer, and core web vitals.

Deno vs Node.js

I would look at using Deno instead of Node.js with a big up-front evaluation. There are a lot of unknowns in running a new service with a new runtime, so it would be really important to feel confident using one vs another.

Node.js is great, tried, and tested and performant enough for even a globally scaled service, but the ergonomics, patterns, and core APIs within Deno make Node.js a bit of a nuisance to maintain. Things have gotten better for sure, but why not re-evaluate if we’ve got the chance?

HTTP server framework

This may come as a surprise, but not a lot of performance gains would come through a slightly faster web server – most of the processing time comes from API requests, looking up users, posts, etc. This one would probably come down to personal/team preference to choose between Express and Fastify.

However, if we end up in Deno land, we’d likely end up using Oak. It looks a lot like Express and Fastify, but native for Deno.

Parting thoughts

While I strongly believe the solutions outlined here would work well, they’re all theoretical and/or opinion-based. If this were actually a project that I would be working on, I would first ensure proof-of-concepts were made for everything and get buy-in from both direct and supporting teams in my organization. Once all evaluations have been made, then we’d move on to writing a technical design before proceeding with building anything more.