perf: optimize PageSpeed Insights performance score (#105) (#106)

This commit implements comprehensive performance optimizations to improve
the PageSpeed Insights score from 70/100 to targeting 90+.

## Images & Assets (Est. 200+ KiB savings)
- Optimize vocalinux.png: 207KB → 44KB (79% reduction)
- Add WebP versions for all logo sizes (32, 64, 96, 192, 512px)
- Create og-image.webp: 274KB → 176KB (36% reduction)
- All WebP images provide ~92% size reduction over PNG

## JavaScript Optimization (Est. 84 KiB savings)
- Lazy load react-syntax-highlighter with next/dynamic
- Created CodeBlock component for async code highlighting
- Reduces initial bundle size by deferring heavy syntax highlighter

## Bundle Configuration
- Disable production source maps (reduces bundle size)
- Add .browserslistrc targeting modern browsers (est. 12 KiB savings)
- Remove legacy browser polyfills (IE11, old Android, etc.)

## Critical Rendering Path (Est. 300ms LCP improvement)
- Add preconnect hint for api.github.com
- Defer Google Analytics to lazyOnload strategy

## Caching Strategy
- Add cache headers configuration for static assets
- Cache-Control: public, max-age=31536000, immutable
- Applied to _next/static/* and image assets

## Accessibility Fixes
- Fix footer color contrast: text-zinc-500 → text-zinc-400
- Add prefers-reduced-motion media query support
- All animations respect user motion preferences

## Expected Impact
- Total estimated LCP reduction: ~1.5-2 seconds
- Bundle size reduction: ~100 KB
- Targeting 90+ performance score (up from 70)
This commit is contained in:
Jatin K Malik
2026-01-30 22:47:59 -08:00
committed by GitHub
parent 4219e3e398
commit 071b7b5da3
19 changed files with 134 additions and 23 deletions
+12
View File
@@ -0,0 +1,12 @@
# Modern browsers for optimal bundle size and performance
# Targets browsers with >0.2% market share, last 2 versions, and not dead
> 0.2%
last 2 versions
not dead
# Exclude very old browsers to reduce polyfill size
# This saves ~12KB in bundle size by removing legacy browser support
not ie 11
not op_mini all
not samsung 4
not android 4
+32 -4
View File
@@ -5,6 +5,31 @@ const config = {
trailingSlash: true,
// Configuration for custom domain deployment
distDir: 'out',
// Configure cache headers for static assets
// This generates a _headers file for platforms like Netlify/Vercel
async headers() {
return [
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/:all*(svg|jpg|jpeg|png|webp|avif|ico|woff|woff2)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
images: {
unoptimized: true,
remotePatterns: [
@@ -21,10 +46,13 @@ const config = {
typescript: {
ignoreBuildErrors: true,
},
productionBrowserSourceMaps: true,
webpack: (config, { isServer }) => {
config.stats = "verbose";
config.devtool = 'source-map'
// Disable source maps in production to reduce bundle size
productionBrowserSourceMaps: false,
webpack: (config, { isServer, dev }) => {
// Only use source maps in development
if (!dev) {
config.devtool = false;
}
// Enhanced node polyfills for postgres and other modules
if (!isServer) {
config.resolve.fallback = {
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+5 -3
View File
@@ -185,18 +185,20 @@ export default function RootLayout({
return (
<html lang="en" className={`${GeistSans.variable} scroll-smooth`} suppressHydrationWarning>
<head>
{/* Preconnect to GitHub API for faster fetch of stars and release info (~300ms improvement) */}
<link rel="preconnect" href="https://api.github.com" />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body>
{/* Google tag (gtag.js) */}
{/* Google tag (gtag.js) - deferred to prioritize page interactivity */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-7NBBNJNNQ7"
strategy="afterInteractive"
strategy="lazyOnload"
/>
<Script id="google-analytics" strategy="afterInteractive">
<Script id="google-analytics" strategy="lazyOnload">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
+11 -16
View File
@@ -34,8 +34,7 @@ import {
ExternalLink,
} from "lucide-react";
import { ThemeToggle } from "@/components/theme-toggle";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { CodeBlock } from "@/components/code-block";
import { useInView } from "react-intersection-observer";
// The one-liner install command (split into three lines for display)
@@ -616,15 +615,14 @@ export default function HomePage() {
<CopyButton text={oneClickInstallCommand} />
</div>
<div className="overflow-x-auto">
<SyntaxHighlighter
<CodeBlock
language="bash"
style={atomOneDark}
className="rounded-lg text-sm sm:text-base"
customStyle={{ margin: 0, maxWidth: '100%', overflowX: 'auto' }}
wrapLongLines={false}
>
{oneClickInstallCommand}
</SyntaxHighlighter>
</CodeBlock>
</div>
</div>
@@ -643,15 +641,14 @@ export default function HomePage() {
<CopyButton text={oneClickInstallWhisperCpu} />
</div>
<div className="overflow-x-auto">
<SyntaxHighlighter
<CodeBlock
language="bash"
style={atomOneDark}
className="rounded-lg text-sm"
customStyle={{ margin: 0, maxWidth: '100%', overflowX: 'auto' }}
wrapLongLines={false}
>
{oneClickInstallWhisperCpu}
</SyntaxHighlighter>
</CodeBlock>
</div>
</div>
@@ -670,15 +667,14 @@ export default function HomePage() {
<CopyButton text={oneClickInstallNoWhisper} />
</div>
<div className="overflow-x-auto">
<SyntaxHighlighter
<CodeBlock
language="bash"
style={atomOneDark}
className="rounded-lg text-sm"
customStyle={{ margin: 0, maxWidth: '100%', overflowX: 'auto' }}
wrapLongLines={false}
>
{oneClickInstallNoWhisper}
</SyntaxHighlighter>
</CodeBlock>
</div>
</div>
@@ -749,15 +745,14 @@ export default function HomePage() {
<CopyButton text={uninstallCommand} />
</div>
<div className="overflow-x-auto">
<SyntaxHighlighter
<CodeBlock
language="bash"
style={atomOneDark}
className="rounded-lg text-sm"
customStyle={{ margin: 0, maxWidth: '100%', overflowX: 'auto' }}
wrapLongLines={false}
>
{uninstallCommand}
</SyntaxHighlighter>
</CodeBlock>
</div>
</div>
</div>
@@ -1119,10 +1114,10 @@ export default function HomePage() {
</div>
<div className="pt-8 border-t border-zinc-800 flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-zinc-500 text-sm">
<p className="text-zinc-400 text-sm">
© {new Date().getFullYear()} Vocalinux. Open-source under GPL-3.0 License.
</p>
<p className="text-zinc-500 text-sm flex items-center gap-1">
<p className="text-zinc-400 text-sm flex items-center gap-1">
Made with <Heart className="h-4 w-4 text-red-500" /> by{" "}
<a
href="https://x.com/intent/user?screen_name=jatinkrmalik"
+62
View File
@@ -0,0 +1,62 @@
"use client";
import React, { useState, useEffect } from "react";
import dynamic from "next/dynamic";
// Lazy load syntax highlighter to reduce initial bundle size (~84KB savings)
const SyntaxHighlighter = dynamic(
() => import("react-syntax-highlighter").then(mod => mod.default),
{
ssr: false,
loading: () => (
<div className="bg-zinc-900 rounded-lg p-4 text-sm">
<pre className="text-green-400 whitespace-pre-wrap">Loading...</pre>
</div>
),
}
);
interface CodeBlockProps {
language: string;
children: string;
className?: string;
customStyle?: React.CSSProperties;
wrapLongLines?: boolean;
}
export function CodeBlock({
language,
children,
className = "",
customStyle,
wrapLongLines = false,
}: CodeBlockProps) {
const [style, setStyle] = useState<any>(null);
useEffect(() => {
// Lazy load the style
import("react-syntax-highlighter/dist/esm/styles/hljs").then(mod => {
setStyle(mod.atomOneDark);
});
}, []);
if (!style) {
return (
<div className={`bg-zinc-900 rounded-lg p-4 text-sm ${className}`} style={customStyle}>
<pre className="text-green-400 whitespace-pre-wrap">{children}</pre>
</div>
);
}
return (
<SyntaxHighlighter
language={language}
style={style}
className={className}
customStyle={customStyle}
wrapLongLines={wrapLongLines}
>
{children}
</SyntaxHighlighter>
);
}
+12
View File
@@ -112,3 +112,15 @@ code::-webkit-scrollbar {
nextjs-portal {
display: none;
}
/* Respect user's motion preferences for accessibility */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}