Goldblog
GitHubSiteTwitter

Speeding Up Centered Part 1: 81 <iframe> Embeds

July 19, 2023

Page performance is an essential part of any web application. Poor page performance results in worsened SEO, user retention, and general website pleasantness. But when youā€™re a startup working quickly to implement critical user features, itā€™s easy to let ā€œperformance paper cutsā€ pile up over time.

āœØ Tip: See web.dev/why-speed-matters for a more thorough explanation of why page performance is important.

Mr. Bean waiting ni front of a yellow field of flowers, checking his watch'
Your users are not Mr. Bean. Don't make them wait to use your website. [source]

Earlier this year, I spent some time working on performance for the popular Centered app. My goal was to improve the appā€™s page load times to help them improve user acquisition and the general experience of using the app. This post is the first in a series describing each of the significant page load performance improvements I made. For each improvement, Iā€™ll cover:

  1. Identification: how I used developer tools at a high level to identify where there was an issue
  2. Investigation: using more tooling to dig deeper into what the root cause of the issue was
  3. Implementation: the actual code change I made to address the performance issue
  4. Confirmation: observing that the fixes improved the production app as expected

The net results were quite pleasing:

  • ~80% reduction in bundle size loaded on all pages
  • ā‰„2 second reduction in all pagesā€™ load script processing

This post covers the first of five key improvements I made to the Centered app:

  1. šŸ‘‰ 81 <iframe> Embeds
  2. Hidden Embedded Images: coming soon!
  3. Unused Code Bloat: coming soon!
  4. Emoji Processing Time: coming soon!
  5. Bucket Imports: coming soon!

Letā€™s dig in!

Centered?

First, letā€™s cover some context: what is Centered and why was I looking into it?

Centered is a web app that provides a suite of tools to help you get into high productivity (ā€œflowā€) states. It provides nice background music, reminders, task tracking, and coach-led group working sessions. Iā€™m a fan - and would recommend trying it out if you havenā€™t yet.

Screenshot of the Freakin' Nerds group in the Centered app. I've messaged 'I love it here'. My task is 'Work on Centered performance blog post part 1'
Using the Centered app, in group lovingly titled "Freakin' Nerds".

Centered has excellent user retention -users who get into it stay using it- but not as good acquisition: getting new users to try it. I was first connected to Centered by Cassidy Williams, an advisor to the Centered team and coach voice on the app, in the context of brainstorming how to improve their user acquisition.

My first instinct as a web developer is to check web apps for serious SEO issues such as poor accessibility or performance.

Issue 1: The Quotes Page

The first issue I looked at on was the centered.app/quotes marketing page. It contains 81 <iframe> embeds of tweets from various happy users. The team had noticed the page takes a long time to load and spikes the userā€™s CPU in the process.

I tried visiting centered.app/quotes, and ā€¦ wow! They were right. It was aggravatingly slow.

Recording of centered.app/quotes with a blank page for several seconds before populating tweets content
2x sped up recording of loading centered.app/quotes

On my Mac Mini with an M1 Max chip, the page took ten seconds to populate page content. Yikes! Users tend to abandon pages after ~2-3 seconds at most, let alone double digits.

Iā€™m guessing this particular page had started off with only a small number of quotes to load, then slowly expanded and slowed down over time.

1. Identification

Normally at the beginning of an investigation Iā€™ll run a tool such as the Chrome dev tools > Lighthouse performance audit to get an overview of a pageā€™s performance issues. But this page was straightforward enough that I felt I could run off my past experiences and intuition.

The pageā€™s HTML, CSS, and JavaScript all loaded pretty quickly: so the issue was client-side, rather than the server taking a long time to send them over. That meant the issue was most likely some client scripts taking too long to run.

Problem identified. Next step: investigating what was running on the client.

2. Investigation

I took a look at the DOM structure of the page to see what was being created. What I saw amused me: dozens upon dozens of <iframe>s, each embedding one of the tweets shown on the page!

Screenshot of centered.app/quotes with Elements dev tools open, showing a masonry grid with one entry (class name 'w-full  shadow-2xl') expanded to show an iframe
Each of the w-full divs contains an <iframe>. That's a lot of <iframe>s.

The page uses the popular react-twitter-embed library to render an <iframe> for each tweet. The <iframe> strategy for embedding tweets is common and handy for avoiding the higher cost approach of using Twitter APIā€™s to populate tweet data in a custom-written component.

Fun fact: each <iframe> in a web page causes the browser to recreate what is essentially a webpage-in-a-webpage, complete with its own DOM body and JavaScript execution environment. Placing several dozen <iframe>s immediately on a single webpage is almost the equivalent of opening that many browser tabs all at the same times.

Even worse, each <iframe>ā€™s JavaScript runs in the same thread as its parent. Having multiple sub-pages run their page startup routines simultaneously can be disastrous for page loading times!

Investigation complete: there were too many <iframe>s loading with the page. Next step: reducing that number.

3. Implementation

One of the most common techniques in performance work is lazy loading: loading content only when it is needed. In web pages, that often means loading only content ā€œabove the foldā€ -content the user can see without scrolling- initially, then loading subsequent content as the user scrolls down. The quotes page only really needed the first half dozen or so tweets to load immediately.

I opted for a variant sometimes called ā€œover-eager loadingā€: loading the initial content first, then ā€œeagerlyā€ loading in more and more content later. You can think of over-eager loading as an optimization over lazy loading. In lazy loading, content isnā€™t loaded until itā€™s needed. In over-eager loading, not-yet-needed content is still not loaded immediately, but it is ā€œpreloadedā€ after the main content.

āœØ Tip: A more common example of over-eager loading is pre-fetching HTTP resources with <link rel="preload">.

The code change roughly boiled down to incrementing a counter in state tracking how many cards should be shown:

+ const startingCardsCount = 6;
+
export default function Cards(): JSX.Element {
  const cards = useSocialCards();

+ const [loadedCount, setLoadedCount] = useState(0)
+ const onCardLoad = () => setLoadedCount((previous) => previous + 1)

  return (
    <>
      {cards
+       // We exponentially increase the number of cards shown as they load in
+       // This way we don't load potentially dozens of <iframe> embeds at once
+       .slice(0, startingCardsCount + loadedCount * 1.5)
        .map((card) => (
            <Card
                key={card.id}
                {...card}
+               onLoad={onCardLoad}
            />
        ))}
    </>
  );
}

I had it start with 6 cards and increment by loadedCount * 1.5 for a couple of reasons:

  • Exponential growth: after the user loaded the first few rows, their browser was likely more ready to load even more
  • Resiliency: a single card failing to load wouldnā€™t stop other cards from incrementing the count

The numbers are arbitrary, but whatā€™s important is that the page was waiting to load later <iframe>s until after earlier ones.

Next step: confirming the pageā€™s improved loading behavior.

4. Confirmation

This change was straightforward to verify. I loaded the quotes page with the code changes and immediately noticed a drastic improvement in perceived load time. The general time from page load to all visible content loading was reduced from >10 seconds to <3 seconds.

Recording of centered.app/quotes with a blank page for several seconds before populating tweets content
1x recording of loading centered.app/quotes with the over-eager loading

The page still has a bit of choppiness as the frames load in. Itā€™d be nice to set up a page system that uses its own React components with cached tweet data instead of any <iframe> embeds. Thatā€™s a longer task for another day. For now, Iā€™m satisfied with the 300% speedup. āš”ļø

Normally at this stage Iā€™d try to validate with real user data on production, ideally by running an A/B test. Seeing if any user behaviors such as bounce rate changed on production could work as a backup. Unfortunately, we didnā€™t have a suitable analytics provider set up at the time.

In Conclusion

Web performance is easy to neglect when youā€™ve got other pressing concerns to handle. But pages that may have been fine one year may slowly turn slow over time.

When your pages grow too slow, strategies such as lazy loading and over-eager loading can trim those bloated webpages back to speedier versions of themselves. Be sure to always investigate the root cause of issues, surgically improve performance bottlenecks, and validate that your changes do what you intend.

Acknowledgments

Many thanks to:

  • Cassidy Williams for connecting us in the first place
  • Steven Puri for great initial chats about the product (plus real enthusiasm about making a great product!) and Michael Cash for helping continue that onboarding
  • Nicolas Carlo for reviewing my PRs into the Centered repository

Coming Soon

In later posts, Iā€™ll dive deeper into using dev tools and more intricate code strategies to detect and fix deeper issues. Look forward to them later this summer! āœØ

Josh GoldbergHi, I'm Josh! I'm a full time independent open source developer. I work on projects in the TypeScript ecosystem such as typescript-eslint and TypeStat. This is my blog about JavaScript, TypeScript, and open source web development.
This site's open source on GitHub. Found a problem? File an issue!