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.
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:
- Identification: how I used developer tools at a high level to identify where there was an issue
- Investigation: using more tooling to dig deeper into what the root cause of the issue was
- Implementation: the actual code change I made to address the performance issue
- 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:
- š 81 <iframe> Embeds
- Hidden Embedded Images: coming soon!
- Unused Code Bloat: coming soon!
- Emoji Processing Time: coming soon!
- 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.
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.
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!
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.
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! āØ