Skip to main content
Published on

The Complete Guide to Achieving 90+ PageSpeed Scores: A Real-World Case Study

Authors
  • avatar
    Name
    Stryxon
    Twitter

Introduction

When we ran our first PageSpeed Insights test on our newly redesigned website, the results were disappointing:

  • 🔴 Performance: 65
  • 🟠 Accessibility: 84
  • 🟠 Best Practices: 73
  • 🟢 SEO: 100

Despite having a modern Next.js stack with fancy animations and beautiful UI, our site was slow, had accessibility issues, and wasn't following security best practices. Sound familiar?

Over the next few days, we systematically addressed each issue. The result? A complete transformation:

  • 🟢 Performance: 90+
  • 🟢 Accessibility: 98+
  • 🟢 Best Practices: 95+
  • 🟢 SEO: 100

In this comprehensive guide, I'll walk you through every optimization we made, why it matters, and how you can implement these changes on your own website. Whether you're using Next.js, React, or any other framework, these principles apply universally.

Understanding Core Web Vitals

Before diving into optimizations, let's understand what Google actually measures:

Largest Contentful Paint (LCP)

Target: < 2.5 seconds

This measures how long it takes for your largest visible element (usually a hero image or heading) to load. Our initial LCP was 3.8s — way too slow.

What kills LCP:

  • Unoptimized images
  • Render-blocking JavaScript
  • Slow server response times
  • Large CSS files

First Input Delay (FID) / Total Blocking Time (TBT)

Target: < 100ms (FID) or < 300ms (TBT)

This measures how quickly your site responds to user interactions. Our TBT was initially 9,930ms (yes, nearly 10 seconds!) due to heavy JavaScript animations blocking the main thread.

What kills FID/TBT:

  • Heavy JavaScript execution on page load
  • Large third-party scripts
  • Unoptimized animations
  • Synchronous code execution

Cumulative Layout Shift (CLS)

Target: < 0.1

This measures visual stability — how much your content shifts around while loading. Fortunately, we had this under control from the start (0.05).

What causes CLS:

  • Images without width/height attributes
  • Ads or embeds without reserved space
  • Web fonts causing layout shifts
  • Dynamic content injection

Image Optimization: The Biggest Win

Impact: Saved 134 KB, improved LCP by 40%

Images were our biggest problem. We had beautiful high-quality images at 95% quality — great for portfolios, terrible for performance.

What We Changed

Before:

TSX
<Image src="/hero-image.jpg" alt="Hero" width={1200} height={630} quality={95} // ❌ Too high />

After:

TSX
<Image src="/hero-image.jpg" alt="Hero" width={1200} height={630} quality={75} // ✅ Sweet spot for web priority={true} // ✅ Preload above-the-fold images />

Key Lessons

  1. Quality 75 is the sweet spot: The human eye can barely distinguish between 75% and 95% quality, but the file size difference is massive.

  2. Enable modern formats: Next.js automatically serves AVIF/WebP to supporting browsers:

JAVASCRIPT
// next.config.js module.exports = { images: { formats: ['image/avif', 'image/webp'], }, }
  1. Use priority wisely: Only mark above-the-fold images as priority={true}. Everything else should lazy-load.

  2. Always provide dimensions: This prevents CLS:

TSX
// ❌ Bad - causes layout shift <Image src="/image.jpg" alt="..." /> // ✅ Good - reserves space <Image src="/image.jpg" alt="..." width={400} height={300} />

Real Numbers

  • Original hero image: 156 KB
  • Optimized (quality 75, AVIF): 22 KB
  • Total savings: 134 KB across all images
  • LCP improvement: 3.8s → 2.2s

Accessibility Fixes That Matter

Impact: Improved score from 84 to 98

Accessibility isn't just about compliance — it's about making your site usable for everyone. Here's what we fixed:

1. Buttons Without Accessible Names

The Problem:

TSX
// ❌ Screen readers don't know what this button does <button onClick={handleSearch}> <SearchIcon /> </button>

The Solution:

TSX
// ✅ Clear purpose for everyone <button onClick={handleSearch} aria-label="Search blog posts"> <SearchIcon /> </button> // ✅ Even better - visible text helps everyone <button onClick={handleSearch}> <SearchIcon /> <span>Search</span> </button>

2. Touch Targets Too Small

Mobile users need buttons they can actually tap. The minimum size is 48x48 pixels (iOS) or 44x44 pixels (Android).

The Problem:

TSX
// ❌ Only 32px tall - hard to tap <button className="p-2"> <Icon /> </button>

The Solution:

TSX
// ✅ 48px minimum for comfortable tapping <button className="min-h-12 min-w-12 p-3"> <Icon /> </button>

3. ARIA Attribute Misuse

We had aria-label on a <p> element, which is prohibited:

The Problem:

TSX
// ❌ Paragraph elements shouldn't have aria-labels <p aria-label="Description">Text content</p>

The Solution:

TSX
// ✅ Remove unnecessary ARIA attributes <p>Text content</p> // ✅ Use ARIA only on interactive/semantic elements <nav aria-label="Main navigation">...</nav> <button aria-label="Close dialog">×</button>

4. Color Contrast

Ensure text has sufficient contrast (minimum 4.5:1 ratio):

TSX
// ❌ Gray on light gray - hard to read <p className="text-gray-400 bg-gray-100">...</p> // ✅ Dark on light - clear and readable <p className="text-gray-800 bg-white">...</p>

Pro Tip: Use WebAIM's Contrast Checker to verify your color combinations.

Quick Accessibility Checklist

  • All images have meaningful alt text
  • All buttons/links have clear labels
  • Touch targets are at least 48x48px
  • Color contrast meets WCAG AA (4.5:1)
  • Forms have proper <label> elements
  • Heading hierarchy is logical (h1 → h2 → h3)
  • Keyboard navigation works everywhere

JavaScript Performance & Modern Browsers

Impact: Reduced bundle size by 11 KB, improved TBT by 65%

The Problem: Legacy JavaScript Polyfills

Our Next.js app was shipping polyfills for Internet Explorer 11, adding 11 KB of unnecessary JavaScript for modern users.

The Solution: Target Modern Browsers Only

Create .browserslistrc:

JavaScript
>0.5% last 2 versions not dead not IE 11 not op_mini all Chrome >= 90 Firefox >= 88 Safari >= 14 Edge >= 90

Update next.config.js:

JAVASCRIPT
module.exports = { webpack: (config, { isServer }) => { if (!isServer) { config.target = ['web', 'es2020'] } return config }, }

This tells webpack to output ES2020 code, which all modern browsers support. You'll automatically get:

  • Smaller bundle sizes
  • Faster parsing
  • Better performance
  • No unnecessary polyfills

Dynamic Imports: Load Code When Needed

Heavy components should be loaded only when needed:

TSX
import dynamic from 'next/dynamic' // ❌ Blocks initial page load import HeavyComponent from '@/components/HeavyComponent' // ✅ Loads only when needed const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), { loading: () => <div>Loading...</div>, ssr: false, // Skip server-side rendering if not needed })

When to use dynamic imports:

  • Comment sections
  • Code syntax highlighters
  • Charts and data visualizations
  • Modal dialogs
  • Share buttons
  • Analytics dashboards

Real Impact

  • Initial JavaScript bundle: 287 KB → 276 KB
  • Total Blocking Time: 9,930ms → 3,400ms
  • Time to Interactive: 8.2s → 4.8s

CSS Animations: GPU Acceleration

Impact: Eliminated non-composited animations warning

The Problem

We had a gradient animation that was killing performance:

CSS
/* ❌ Triggers expensive repaints */ @keyframes gradient-shift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }

Every frame, the browser had to recalculate layout and repaint the entire element.

The Solution: Use Transform & Opacity

Only two CSS properties can be animated without triggering layout/paint: transform and opacity. These run on the GPU.

CSS
/* ✅ GPU-accelerated, buttery smooth */ @keyframes gradient-shift { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.05); opacity: 0.9; } 100% { transform: scale(1); opacity: 1; } } .animated-element { will-change: transform, opacity; transform: translateZ(0); /* Force GPU layer */ }

Animation Performance Rules

✅ DO:

  • Use transform for movement (translateX, translateY, scale, rotate)
  • Use opacity for fading
  • Add will-change: transform, opacity for animations
  • Use transform: translateZ(0) to force GPU rendering

❌ DON'T:

  • Animate top, left, right, bottom
  • Animate width, height
  • Animate margin, padding
  • Animate background-color (use opacity on overlay instead)
  • Animate box-shadow (use opacity on pseudo-element instead)

Security Headers: Protect Your Users

Impact: Improved Best Practices score from 73 to 95

Security headers protect your users from common web vulnerabilities. Here's what we implemented:

Essential Security Headers

next.config.js:

JAVASCRIPT
module.exports = { async headers() { return [ { source: '/:path*', headers: [ // Prevent clickjacking attacks { key: 'X-Frame-Options', value: 'SAMEORIGIN', }, // Prevent MIME type sniffing { key: 'X-Content-Type-Options', value: 'nosniff', }, // Enable XSS protection { key: 'X-XSS-Protection', value: '1; mode=block', }, // Force HTTPS (after enabling SSL) { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload', }, // Control what resources can load { key: 'Content-Security-Policy', value: [ "default-src 'self'", "script-src 'self' 'unsafe-eval' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self' https:", ].join('; '), }, // Control what features can be used { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()', }, ], }, ] }, }

What Each Header Does

X-Frame-Options: SAMEORIGIN
Prevents your site from being embedded in iframes on other domains (clickjacking protection).

X-Content-Type-Options: nosniff
Prevents browsers from trying to "guess" the MIME type of your files, which could lead to security vulnerabilities.

Strict-Transport-Security (HSTS)
Forces browsers to only connect via HTTPS, even if the user types http:// in the address bar.

Content-Security-Policy (CSP)
Controls what resources can load on your page. This is your strongest defense against XSS attacks.

Permissions-Policy
Prevents your site (or embedded iframes) from accessing sensitive features like camera, microphone, or geolocation without explicit permission.

Testing Your Headers

Use securityheaders.com to scan your site and get a grade.

SEO Setup: Beyond the Basics

Impact: Maintained 100/100 SEO score, improved search visibility

We already had a 100 SEO score, but there's more to SEO than what PageSpeed tests. Here's our complete SEO setup:

1. Structured Data (JSON-LD)

Search engines love structured data. It helps them understand your content and show rich snippets in search results.

Organization Schema:

TSX
// app/layout.tsx export default function RootLayout({ children }) { const jsonLd = { '@context': 'https://schema.org', '@type': 'Organization', name: 'Stryxon Technologies', url: 'https://www.stryxon.com', logo: 'https://www.stryxon.com/images/logo.png', sameAs: ['https://twitter.com/stryxon', 'https://linkedin.com/company/stryxon'], } return ( <html> <head> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> </head> <body>{children}</body> </html> ) }

Service Page Schema:

TSX
// app/services/[service]/page.tsx const jsonLd = { '@context': 'https://schema.org', '@graph': [ { '@type': 'Service', name: 'Cloud Infrastructure Services', provider: { '@type': 'Organization', name: 'Stryxon Technologies', }, description: 'Professional cloud infrastructure services...', }, { '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://www.stryxon.com', }, { '@type': 'ListItem', position: 2, name: 'Services', item: 'https://www.stryxon.com/services', }, { '@type': 'ListItem', position: 3, name: 'Cloud Infrastructure', item: 'https://www.stryxon.com/services/cloud-infrastructure', }, ], }, ], }

2. Open Graph & Twitter Cards

Make your site look great when shared on social media:

TSX
// app/layout.tsx export const metadata = { metadataBase: new URL('https://www.stryxon.com'), title: { default: 'Stryxon Technologies - Web Development & Cloud Infrastructure', template: '%s | Stryxon Technologies', }, description: 'Professional web development and cloud infrastructure services...', openGraph: { type: 'website', locale: 'en_US', url: 'https://www.stryxon.com', siteName: 'Stryxon Technologies', images: [ { url: '/og-image.png', // 1200x630px image width: 1200, height: 630, alt: 'Stryxon Technologies', }, ], }, twitter: { card: 'summary_large_image', images: ['/og-image.png'], }, }

Pro Tip: Create your og-image at exactly 1200x630px. Test how it looks using Facebook's Sharing Debugger.

3. Sitemap & Robots.txt

Dynamic Sitemap (app/sitemap.ts):

TYPESCRIPT
import { MetadataRoute } from 'next' export default function sitemap(): MetadataRoute.Sitemap { return [ { url: 'https://www.stryxon.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1, }, { url: 'https://www.stryxon.com/services/web-development', lastModified: new Date(), changeFrequency: 'weekly', priority: 0.9, }, // Add all your pages here ] }

Robots.txt (app/robots.ts):

TYPESCRIPT
import { MetadataRoute } from 'next' export default function robots(): MetadataRoute.Robots { return { rules: [ { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/'], }, // Block AI crawlers if you don't want them training on your content { userAgent: ['GPTBot', 'ChatGPT-User', 'CCBot'], disallow: ['/'], }, ], sitemap: 'https://www.stryxon.com/sitemap.xml', } }

4. Search Console Verification

Verify your site with all major search engines:

Google Search Console:

TSX
export const metadata = { verification: { google: 'your-google-verification-code', }, }

Bing Webmaster Tools:

TSX
export const metadata = { verification: { other: { 'msvalidate.01': 'your-bing-verification-code', }, }, }

Yandex Webmaster:

TSX
export const metadata = { verification: { other: { 'yandex-verification': 'your-yandex-code', }, }, }

After verification, submit your sitemap to each search engine for faster indexing.

Analytics & Monitoring

Impact: Real-time insights into actual user experience

PageSpeed Insights shows lab data (simulated tests). Real user monitoring shows what your actual users experience.

Vercel Analytics & Speed Insights

The easiest solution for Next.js apps:

Bash
npm install @vercel/analytics @vercel/speed-insights
TSX
// app/layout.tsx import { Analytics } from '@vercel/analytics/react' import { SpeedInsights } from '@vercel/speed-insights/next' export default function RootLayout({ children }) { return ( <html> <body> {children} <Analytics /> <SpeedInsights /> </body> </html> ) }

This gives you:

  • Real Core Web Vitals from actual users
  • Geographic distribution
  • Device breakdown (mobile vs desktop)
  • Page-by-page performance
  • Conversion tracking

What to Monitor

Core Web Vitals (75th percentile):

  • LCP: < 2.5s 🟢 Good
  • FID: < 100ms 🟢 Good
  • CLS: < 0.1 🟢 Good

Other Metrics:

  • Page load time by country
  • Bounce rate on slow pages
  • Conversion rate correlation with speed
  • Error rates

Cloudflare CDN Configuration

Impact: 40% faster load times globally

A CDN (Content Delivery Network) serves your content from servers close to your users. We use Cloudflare's free tier.

DNS Setup

  1. Add your site to Cloudflare
  2. Update your domain's nameservers to Cloudflare's
  3. Enable the orange cloud (proxy) for your domain records

Page Rules (Free Tier: 3 rules)

Rule 1: Cache Everything (Home Page)

  • URL: www.stryxon.com/
  • Settings: Cache Level = Cache Everything, Edge Cache TTL = 1 day

Rule 2: Cache Static Assets

  • URL: www.stryxon.com/_next/static/*
  • Settings: Cache Level = Cache Everything, Edge Cache TTL = 1 year

Rule 3: Force HTTPS

  • URL: http://www.stryxon.com/*
  • Settings: Always Use HTTPS = On

Speed Settings

Go to SpeedOptimization:

  • ✅ Auto Minify: HTML, CSS, JavaScript
  • ✅ Brotli compression
  • ✅ Early Hints
  • ❌ Rocket Loader (turn off - breaks React hydration)

Security Settings

Go to Security:

  • Security Level: Medium
  • Challenge Passage: 30 minutes
  • Browser Integrity Check: On

Results & Key Takeaways

Before & After Comparison

MetricBeforeAfterImprovement
Performance6590++38%
Accessibility8498+17%
Best Practices7395+30%
SEO100100Maintained
LCP3.8s2.2s-42%
TBT9,930ms3,400ms-66%
CLS0.050.03-40%
Bundle Size287 KB276 KB-11 KB

Time Investment

  • Total time spent: ~8 hours over 3 days
  • Most impactful changes done in first 2 hours
  • Worth every minute for the performance gains

What Had the Biggest Impact

  1. Image optimization (40% of improvement)
  2. JavaScript bundle size (25% of improvement)
  3. Accessibility fixes (20% of improvement)
  4. Security headers (10% of improvement)
  5. Other optimizations (5% of improvement)

Key Lessons Learned

1. Don't chase perfection
You don't need 100/100 on every metric. 85+ is excellent and achieves diminishing returns beyond that point.

2. Start with images
Images are usually the biggest performance killer. Fix these first for maximum impact.

3. Mobile users matter most
60%+ of traffic is mobile. Test and optimize for mobile first.

4. Real users > lab tests
PageSpeed Insights is great for catching issues, but real user monitoring (via Vercel Analytics) shows what actually matters.

5. Accessibility benefits everyone
Larger touch targets, clear labels, and good contrast don't just help disabled users — they make your site easier for everyone.

6. Security is not optional
Implement security headers from day one. It's easier to do upfront than to retrofit later.

Your Action Plan

Ready to optimize your site? Follow this prioritized checklist:

Week 1: Quick Wins

  • Reduce image quality to 75%
  • Enable AVIF/WebP in Next.js config
  • Add priority={true} to hero images
  • Fix any accessibility warnings
  • Add security headers

Week 2: JavaScript Optimization

  • Create .browserslistrc for modern browsers
  • Identify and lazy-load heavy components
  • Remove unused dependencies
  • Analyze bundle with next bundle-analyzer

Week 3: SEO & Monitoring

  • Add structured data (JSON-LD)
  • Create dynamic sitemap
  • Verify with Google/Bing/Yandex
  • Install Vercel Analytics
  • Set up Cloudflare CDN

Week 4: Polish & Monitor

  • Test on real devices
  • Fix remaining issues
  • Monitor Core Web Vitals
  • Celebrate your new scores! 🎉

Tools & Resources

Testing Tools

Accessibility

SEO

Performance

Conclusion

Optimizing your website for performance, accessibility, and SEO doesn't have to be overwhelming. By systematically addressing each issue — starting with the biggest wins like image optimization — you can transform your site's scores in just a few days.

Remember:

  • Performance affects conversions: Amazon found that every 100ms of latency cost them 1% in sales
  • Accessibility is the law: Many countries require WCAG compliance
  • SEO drives organic traffic: Better technical SEO means better rankings
  • Users notice speed: 53% of mobile users abandon sites that take over 3 seconds to load

The best time to optimize was when you built your site. The second best time is now.


About the Author

This guide is based on our real-world experience optimizing blog.stryxon.com from scratch. We documented every step, challenge, and solution so you don't have to learn the hard way.

Published: February 2026
Reading Time: 15 minutes
Difficulty: Intermediate