As promised in the last blog post, in this one, I will explain how Confluence achieved rendering the page 2x faster in only one year - FY25.
If you read my previous post, you’ve seen that we deployed SSR (Server-Side Rendering) a few years back. We are using FMP mark to measure when the server-side rendered HTML has been downloaded and the browser has created the DOM out of it. After that, React JavaScript runs and basically recreates the DOM, makes everything interactive and renders any additional content that wasn’t or couldn’t be SSRed. This includes some legacy 1st party macros as well as Connect and Forge 3rd party ones, among other things. When everything in the viewport is done loading and stable, TTVC metric is fired.
As you can see, to improve page load time, we need to improve SSR latency as well as reduce the time it takes to complete client-side rendering. I will be focusing on the latter in this blog post.
In the beginning of last fiscal year, we were using React 16. Moving to React 18 (R18) was a major undertaking. As you can imagine, we have a large codebase with a lot of dependencies across multiple teams in Atlassian, as we have a portfolio of Platform Components we are using, and all had to be upgraded (400 incompatible packages, 1600 tests to be fixed, 4000 test cases to be migrated—just to give an idea). R18 unlocked many opportunities, such as concurrent reading, hydration, and streaming.
These brought major out-of-the-box performance enhancements that made Confluence load faster and feel more responsive. Immediate access to new React features designed to help developers further enhance perceived performance, including tools like React Transitions, Server Components, better performance tooling, and more. This also unlocked the ability to introduce streaming (more details in the next blog post).
With hydration, React preserves the HTML content rendered during SSR and attaches event handlers directly to the existing DOM elements, making the initial render more performant. However, in order to fully realize the performance gains of the new render API, we had to ensure that the server and client DOM tree matched. This introduced a lot of work due to existing architecture.
We have TTVC instrumentation that allows us to see which elements on the page are changing the DOM. Using this instrumentation, we were able to identify a lot of opportunities; here a few for illustration.
Tables have an extremely high usage rate and were our #1 most impactful TTVC offender. They were rearchitected to use native CSS positioning instead of JS, a complex change requiring extensive testing due to dozens of table variants and edge cases.
Before - table shifts during loading |
|
---|
Complex pages with a significant number of macros often would freeze the page, leading to customer escalations. We rearchitected the flow to fetch and render them in batches instead of all at once. We saw a visual improvement in loading of macros and improved TBT in P90 by 19%.
Improved macro loading (left = before showing app loading delays, right = after)
Confluence fixed 100+ shifting elements on pages (we call them TTVC long poles). To achieve this, many strategies were employed, including:
Reducing prevalence of slow DOM mutations, re-renders, and re-mounts
Introducing additional preloading for critical interactions and flows
Improving API latency of critical endpoints
The internal blog post that summarized the above wins was called “We made Confluence 2x faster in FY25, but we're not done yet!”. We aim to continue investing to further improve Confluence performance. In the blog posts that will follow, I want to talk more about how we think about various performance aspects—React 18 streaming, how we are improving SSR, and more. Stay tuned!
Ilia Fainshtroy
0 comments