AG

Landing Speed Optimization

Case Study showing how I optimize landings for faster load times and improved user experience

Background

The company manages a large portfolio of landing pages, each tailored to specific user activity and engagement. These pages play a crucial role in guiding users toward relevant advertisements that match their interests.

With dozens of landing variations and multiple designs for each, the challenge is ensuring these pages perform seamlessly for a global audience. Users from different regions rely on varying internet speeds and devices, making optimization essential for delivering a smooth experience everywhere.

Key Challenges

  • Global Audience Needs: Landings must load quickly and efficiently, regardless of a user's location or device capability.
  • Traffic Volume and Revenue Constraints: Due to the high volume of traffic and low revenue per user, server-side rendering (SSR) is not feasible. The solution must rely on alternative methods to deliver exceptional performance at scale.

Current Project Setup

  • Framework:

    React SPA


  • State Management:

    Redux


  • Styling:

    CSS preprocessors


  • Build Process:

    Custom Webpack config


Problems

Technology Stack Issues


React
While powerful, it is overly heavy for the project's needs, especially given the scale and performance requirements.

Redux
Redux adds significant weight and complexity, making it less than ideal for lightweight, high-performance applications.

Sass
Functional and reliable, but it lacks the simplicity needed for seamless theme styling and inlining of critical CSS.
No SSG
The current setup is entirely client-side, with no support for server-side rendering or static generation. This limits performance optimization and increases load times.

General Issues

White Empty Screen
A white empty screen is the default starting point for a React SPA, but it's far from ideal. In regions with slower internet connections, it can take several seconds for meaningful content to appear. This delay increases the likelihood that users will abandon the site before it even loads. The code below is a usual CDN response for a React SPA.
Fetch-As-You-Render Limitations
In scenarios where server-side rendering (SSR) is not feasible—due to high traffic volume and low revenue per user—static content delivered via a CDN is the more cost-effective solution. However, without SSR, the client must fetch additional JavaScript based on search parameters or pathnames after loading the initial page. These extra round trips degrade performance.

Huge JavaScript Bundles Delivery
Single-page applications (SPAs) depend heavily on the user's internet connection and device capabilities. In areas with slower networks or less powerful devices, downloading and executing large amounts of JavaScript leads to slow loading times or even complete failure to load the landing page.
Slow Initial Rendering
Heavy reliance on JavaScript during the initial load create a frustrating user experience, particularly for users with slower internet speeds or older devices. This early performance bottleneck increases bounce rates and risks making the app inaccessible to a segment of your audience.

Solution

Optimizing the Technology Stack


Replace React with Preact
Switch from React (55kb) to Preact (9kb) for a significantly lighter and faster solution without compromising essential functionality.
Simplify State Management
Use useReducer and useContext for state management instead of Redux. This avoids adding unnecessary library overhead and keeps the bundle size lean.

Enable Static Site Generation (SSG)
Utilize Next.js (version 12) to support static site generation. While newer versions of Next.js don't yet support Preact, version 12 provides the balance needed for SSG compatibility.

Adopt Styled Components
Replace Sass with styled-components for easier theme creation and inline CSS injection. Combined with Next.js, this approach simplifies theming while improving performance by moving critical CSS to the document head.

Note

After 18 months of experience with this stack, I've identified a more efficient solution. I will reveal it at the end of the page

General Improvements

Use Static Routes for Landing Designs

Generate all possible landing page designs as static routes during the build process. This approach ensures the page content does not rely on dynamic search parameters, which can degrade performance.

Reduce Round Trips Between Client and CDN

When server-side rendering isn't an option, prerendering static routes minimizes client-server communication. This reduces the number of round trips and speeds up the final page load.

Prerender HTML and Route Data

By prerendering routes, data, and initial HTML with content during the build step, users can see the landing page instantly from the CDN's response. The faster users see content, the more likely they are to engage with the page.

Eliminate Redundant Data Fetching

Providing the necessary data at build time avoids additional client-side requests for dynamic data. This further reduces back-and-forth trips, ensuring faster overall performance.

Here's an example of how you can prerender routes using the following dummy code:

Note

This example uses Next.js version 12, as it's the latest version compatible with Preact. In newer versions of Next.js, you can achieve the same functionality with React Server Components (recommended).

Minimize JavaScript Sent to the Client

Whenever possible, do as much processing as you can during the build phase to reduce the amount of JavaScript sent to the client.

The Example

You have a landing page with multiple designs (10+)
In your React code, you use styled-components to calculate a theme based on a design ID
You pass this theme to a ThemeProvider
During the build step:
  • The code is executed (prerendered)
  • The theme is calculated
  • The initial HTML is generated with the proper theme for each route
Sounds great, right? It's good, but not perfect.

The Issue

Even if you prerender the correct HTML for a specific design, React still sends all the code for every design to the client and re-executes it.
Why? Because in React, any code used during rendering—such as the getTheme function—is shipped to the client and executed again during hydration. This means that a landing page for one design also includes the code for all the other designs, even though it's not needed.

But Didn't We Prerender the Route?

Yes, prerendering produces the correct initial HTML. However, React's hydration process requires sending all the JavaScript to the client, so the size of the JavaScript bundle doesn't shrink just because you prerender.

The Solution

How Can We Reduce the JS Code Sent to the Client?
The solution is simple: do more work outside of React (client components) and provide the final results directly to your React components.
For example, with the theme calculation:
  • During the build: Since the design ID is known, you can calculate the theme using the getTheme function on the server and pass the result to your component.
  • In Next.js (pre-version 13): Use getStaticProps to perform this work during the build.
  • In Next.js (with the app router): Leverage React Server Components (RSC) to call your function and pass the calculated theme to the client component.
  • In Astro: Use Astro components, which are always server-side, to perform the theme calculation.

Example belows shows how one would calculate the theme in Astro and pass it a framework component:

The Benefits

By calculating the theme server-side, you eliminate unnecessary code from the JavaScript bundle and avoid redundant computations on the client.
This not only reduces the bundle size but also improves the overall performance of your application.

Impact

Smaller JavaScript Bundle

Reduced JS bundle size significantly improves performance.

Faster Load Times

Pages load quicker, enhancing the user experience.

Improved User Experience

Landings feel faster and more responsive to users.

Lower Bounce Rates

Fewer users abandon the site during loading, leading to higher engagement.

Increased Revenue

Better performance drives more traffic to landings, resulting in greater cash flow and higher profits for the business.

Reduced Hosting Costs

A leaner bundle decreases bandwidth usage and can lower server expenses over time.

Different Stack Solution


Astro Instead of Next.js
The latest version of Astro offers significant advantages over being tied to Next.js 12. Astro allows the use of any front-end framework, providing greater flexibility. Unlike Next.js, which adds 34kb to the client for routing functionality often unnecessary for simple landings, Astro keeps things lightweight and efficient.

Continue Using Preact
Preact remains an excellent choice. With Astro, it's easy to integrate the compact version of Preact, which adds only 4-5kb to the client bundle, keeping the application fast and lightweight.

Zustand for State Management
Replace useContext with Zustand, a lightweight, framework-agnostic state management solution. Since Astro doesn't support React context wrappers, Zustand provides an elegant and simple alternative.

Tailwind CSS Instead of Styled Components
Tailwind is a better choice for styling in this stack. Styled-components are heavier and rely on React context, which Astro doesn't support. Tailwind, with its atomic CSS approach, is much lighter and allows developers to build landings faster. Additionally, tools like tailwind-merge make it easy to create and manage themes efficiently.