Frontend Performance Optimization - Lessons from the Trenches

What actually worked (and what didn't) in making web apps faster

By Hank Kim

Frontend Performance Optimization - Lessons from the Trenches

What actually worked (and what didn't) in making web apps faster

Frontend Performance Optimization - Lessons from the Trenches

Working on a React application that performed well in development but struggled in production taught me a lot about performance optimization. Users were experiencing slow load times, and I needed to find practical solutions.

Here’s what I learned from optimizing real applications, including which techniques provided the most impact.

The Game Changers That Actually Worked

React Query: When Caching Finally Made Sense

I was working on a dashboard where users frequently switched between tabs, causing redundant API calls each time. The network tab showed repeated requests for the same data, users saw loading spinners constantly, and we were hitting API rate limits.

React Query solved this problem effectively. I had heard about it before but hadn’t fully understood its caching capabilities.

The improvement was significant. Instead of fetching user data on every page navigation, React Query served cached data instantly while updating in the background.

The measurable results:

  • API calls reduced by approximately 60%
  • Loading spinners eliminated for cached data
  • Noticeably faster user experience
// Before: Every single page visit = new API call
const UserProfile = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchUser(userId).then(setUser).finally(() => setLoading(false));
  }, [userId]);
  
  if (loading) return <LoadingSpinner />;
  return <div>{user.name}</div>;
};

// After: Instant cache hits, background updates
const UserProfile = () => {
  const { data: user, isLoading } = useQuery(['user', userId], () => fetchUser(userId));
  
  if (isLoading) return <LoadingSpinner />;
  return <div>{user.name}</div>;
};

The Bundle Size Wake-Up Call

Running a bundle analyzer revealed that my React app’s initial load was 2.3MB, which explained the slow performance for users on slower connections.

I implemented code splitting and lazy loading, which produced significant improvements:

  • Initial bundle went from 2.3MB to under 800KB
  • Lighthouse performance score jumped from 65 to 92
  • Pages that used to take 4+ seconds now loaded in under 2 seconds
// The magic of lazy loading components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

For images, I had gallery pages loading many high-resolution images at once. Adding loading="lazy" was a simple change that significantly reduced load times.

The Prefetching Experiment

I noticed users often hovered over navigation links before clicking, so I implemented prefetching on hover events.

This made page transitions feel instant since the data was already loaded. However, I learned that prefetching needs to be strategic - doing it everywhere can actually hurt performance.

The SEO Performance Connection

Working on SEO improvements also helped performance scores. Properly configuring robots.txt and adding meaningful meta tags improved my Lighthouse scores.

Google’s PageSpeed scores consider the overall user experience, not just raw performance metrics. These SEO optimizations increased my PageSpeed score from 78 to 95.

Other Techniques Worth Trying

Here are additional techniques that have proven effective in performance optimization:

Modern Image Formats: The Low-Hanging Fruit

WebP and AVIF formats can reduce image sizes by 25-50% without visible quality loss. The <picture> element provides fallbacks for older browsers that don’t support these formats.

Service Workers: The Nuclear Option

Service Workers enable aggressive resource caching for near-instant load times. They’re particularly valuable for PWAs but add complexity that may not be necessary for simpler applications.

Critical CSS: The Render Blocker Killer

Inlining the CSS needed for above-the-fold content can eliminate render-blocking resources. I’ve seen this improve First Contentful Paint by 20-30%, but it requires tooling to automate.

CDNs: Geography Matters

Content Delivery Networks can reduce loading times by 40-60% for global users by serving content from geographically closer servers. Services like Cloudflare make CDNs accessible for smaller applications.

Bundle Analysis: Know Your Dependencies

Regular bundle analysis helps identify unused dependencies. A single utility library might import much more code than needed. Tools like webpack-bundle-analyzer show exactly how bundle size is distributed.

The Metrics That Actually Matter

When measuring performance, focus on metrics that directly impact user experience rather than vanity metrics.

Focus on these Core Web Vitals:

  • Largest Contentful Paint (LCP): Under 2.5 seconds (this is what users actually feel)
  • First Input Delay (FID): Under 100ms (can users interact with your app?)
  • Cumulative Layout Shift (CLS): Under 0.1 (stop making content jump around)

While Lighthouse scores are useful benchmarks, real user metrics provide more accurate insights into actual performance.

Where to Start (Priority Order)

Based on impact versus effort, here’s a practical implementation order:

  1. Image lazy loading - Simple to implement with immediate results
  2. Code splitting - High impact once you set it up
  3. React Query or SWR - Game changer for data-heavy apps
  4. Bundle analysis - Find the low-hanging fruit first
  5. Prefetching - Good for polish, but get the basics right first

The Real Talk on Performance

Performance optimization should focus on improving user experience rather than achieving perfect scores. Many significant improvements come from straightforward changes like image lazy loading, code splitting, and API response caching.

The key is measuring performance improvements with real user metrics, not just synthetic tests. Advanced techniques like Service Workers and critical CSS can be valuable, but establishing solid fundamentals first provides the most impact.

Users notice and appreciate fast-loading applications, making performance optimization one of the most worthwhile investments in user experience.