This guide covers performance testing procedures, optimization strategies, and monitoring for the Betirement website.
Run Lighthouse audits on all major pages to measure performance, accessibility, best practices, and SEO.
# Install dependencies (first time only)
npm install --save-dev lighthouse chrome-launcher
# Run audits on local development server
npm run dev
# In another terminal:
node scripts/lighthouse-audit.js http://localhost:3000 mobile
# Run audits on production
node scripts/lighthouse-audit.js https://betirement.com mobile
# Run desktop audits
node scripts/lighthouse-audit.js https://betirement.com desktopReports are saved to lighthouse-reports/[timestamp]/:
- Individual HTML reports for each page
- Summary JSON with all scores
- Summary markdown with recommendations
Target Scores:
- Performance: 90+
- Accessibility: 95+
- Best Practices: 95+
- SEO: 95+
Test page load times under various network conditions to ensure good performance for all users.
# Install Puppeteer (first time only)
npm install --save-dev puppeteer
# Run slow connection tests
node scripts/test-slow-connection.js http://localhost:3000Network Profiles Tested:
- Slow 3G: 500 Kbps, 400ms latency
- Fast 3G: 1.6 Mbps, 150ms latency
- 4G: 4 Mbps, 50ms latency
Target Load Times:
- Slow 3G: < 10s
- Fast 3G: < 5s
- 4G: < 3s
Analyze JavaScript bundle size to identify optimization opportunities.
# Build the project first
npm run build
# Analyze bundle
node scripts/analyze-bundle.jsBundle Size Targets:
- First Load JS: < 200KB
- Individual page bundles: < 100KB
- Shared chunks: < 150KB
Target: < 2.5 seconds
Optimization Strategies:
- Optimize images with Next.js Image component
- Use priority loading for above-fold images
- Minimize render-blocking resources
- Use CDN for static assets
- Implement proper caching
Implementation:
// Priority loading for hero images
<Image
src="/images/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
quality={85}
/>
// Lazy loading for below-fold images
<Image
src="/images/content.jpg"
alt="Content"
width={800}
height={400}
loading="lazy"
/>Target: < 100 milliseconds
Optimization Strategies:
- Minimize JavaScript execution time
- Use code splitting and dynamic imports
- Defer non-critical JavaScript
- Optimize third-party scripts
- Use web workers for heavy computations
Implementation:
// Dynamic imports for heavy components
const VideoPlayer = dynamic(() => import('@/components/VideoPlayer'), {
loading: () => <Skeleton />,
ssr: false,
});
// Defer third-party scripts
<Script
src="https://example.com/script.js"
strategy="lazyOnload"
/>Target: < 0.1
Optimization Strategies:
- Always specify image dimensions
- Reserve space for dynamic content
- Avoid inserting content above existing content
- Use CSS aspect-ratio for responsive images
- Preload fonts to prevent FOIT/FOUT
Implementation:
// Always specify dimensions
<Image
src="/image.jpg"
alt="Description"
width={800}
height={600}
/>
// Reserve space for dynamic content
<div className="min-h-[400px]">
{loading ? <Skeleton /> : <Content />}
</div>
// Use aspect-ratio for responsive containers
<div className="aspect-video">
<iframe src="..." />
</div>Next.js automatically splits code by route, but you can optimize further:
// Heavy components
const Calculator = dynamic(() => import('@/components/calculators/RetirementCalculator'));
const VideoPlayer = dynamic(() => import('@/components/VideoPlayer'));
const Chart = dynamic(() => import('@/components/Chart'));
// Modal components
const EmailGateModal = dynamic(() => import('@/components/content/EmailGateModal'));
const ExitIntentPopup = dynamic(() => import('@/components/sections/ExitIntentPopup'));// app/content/videos/page.tsx
export default async function VideosPage() {
// This page's code is automatically split
return <VideoLibrary />;
}Ensure proper tree shaking by:
- Using ES6 imports
- Avoiding default exports for utilities
- Using named imports from libraries
// Good - tree shakeable
import { formatDate, formatCurrency } from '@/lib/utils';
// Bad - imports entire library
import _ from 'lodash';
// Good - imports only what's needed
import debounce from 'lodash/debounce';# Check for large dependencies
npm list --depth=0
# Analyze what's in your bundle
npm run build
node scripts/analyze-bundle.js- Use
date-fnsinstead ofmoment - Use native browser APIs instead of
lodashwhere possible - Use
clsxinstead ofclassnames - Use
lucide-reactinstead ofreact-icons(already implemented)
All images should use Next.js Image component:
import Image from 'next/image';
<Image
src="/images/photo.jpg"
alt="Description"
width={800}
height={600}
quality={85}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>Configuration in next.config.mjs:
images: {
formats: ['image/webp', 'image/avif'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
}Configuration in netlify.toml:
[[headers]]
for = "/images/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/_next/static/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"// app/api/videos/route.ts
export async function GET() {
const videos = await fetchVideos();
return NextResponse.json(videos, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}// app/content/videos/page.tsx
export const revalidate = 3600; // Revalidate every hour
export default async function VideosPage() {
const videos = await getVideos();
return <VideoLibrary videos={videos} />;
}Already implemented with next/font:
// app/layout.tsx
import { Inter, Open_Sans } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});Already integrated - monitors Core Web Vitals automatically:
// app/layout.tsx
import { VercelAnalytics } from '@/src/components/analytics';
<VercelAnalytics />Custom event tracking for performance monitoring:
import { trackEvent } from '@/src/lib/analytics';
// Track slow page loads
if (loadTime > 3000) {
trackEvent('slow_page_load', {
page: window.location.pathname,
loadTime,
});
}
// Track resource errors
window.addEventListener('error', (e) => {
if (e.target instanceof HTMLImageElement) {
trackEvent('image_load_error', {
src: e.target.src,
});
}
});Set up continuous monitoring with Lighthouse CI:
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
- run: npm install -g @lhci/cli
- run: lhci autorunSet performance budgets to prevent regressions:
// lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"total-blocking-time": ["error", { "maxNumericValue": 300 }]
}
}
}
}- Run Lighthouse audits on all major pages
- Test on slow 3G connection
- Analyze bundle size
- Verify all images use Next.js Image component
- Check for unused dependencies
- Verify caching headers are set correctly
- Test Core Web Vitals on real devices
- Set up performance monitoring
- Configure performance budgets
- Test with JavaScript disabled (progressive enhancement)
- Monitor Core Web Vitals in production
- Set up alerts for performance regressions
- Review analytics for slow pages
- Conduct monthly performance audits
- Update dependencies regularly
- Monitor bundle size with each deployment
- Review and optimize based on real user data
Causes:
- Large images above the fold
- Render-blocking resources
- Slow server response time
Solutions:
- Use priority loading for hero images
- Optimize image sizes
- Implement proper caching
- Use CDN for static assets
Causes:
- Images without dimensions
- Dynamic content insertion
- Web fonts causing FOUT
Solutions:
- Always specify image dimensions
- Reserve space for dynamic content
- Use font-display: swap
- Preload critical fonts
Causes:
- Heavy dependencies
- Lack of code splitting
- Unused code
Solutions:
- Use dynamic imports
- Replace heavy dependencies
- Enable tree shaking
- Remove unused code
Causes:
- No caching
- Inefficient queries
- Cold starts
Solutions:
- Implement caching strategy
- Use ISR for semi-static content
- Optimize API queries
- Use edge functions