Skip to content

Commit 1c28182

Browse files
committed
feat: add Lighthouse performance auditing and image optimization scripts
1 parent aa8635b commit 1c28182

38 files changed

+15257
-3175
lines changed

index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
/>
1212
<link rel="apple-touch-icon" href="logo192.png" />
1313
<link rel="manifest" href="manifest.json" />
14+
15+
<!-- Resource hints for performance -->
16+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
17+
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
18+
1419
<title>Portfolio</title>
1520
</head>
1621
<body>

package-lock.json

Lines changed: 14747 additions & 3163 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
"build": "tsc --noEmit && vite build",
3737
"preview": "vite preview",
3838
"predeploy": "npm run build",
39-
"deploy": "gh-pages -d build"
39+
"deploy": "gh-pages -d build",
40+
"lighthouse": "node scripts/lighthouse.js",
41+
"lighthouse:mobile": "node scripts/lighthouse.js --mobile"
4042
},
4143
"eslintConfig": {
4244
"extends": [
@@ -63,11 +65,20 @@
6365
"@vitejs/plugin-react": "^5.0.2",
6466
"autoprefixer": "^10.4.20",
6567
"axe-playwright": "^2.2.2",
68+
"critters": "^0.0.23",
6669
"gh-pages": "^6.3.0",
70+
"glob": "^11.0.3",
71+
"lighthouse": "^12.8.2",
6772
"playwright": "^1.55.0",
6873
"postcss": "^8.4.49",
74+
"sharp": "^0.34.3",
6975
"tailwindcss": "^3.4.16",
76+
"terser": "^5.44.0",
7077
"vite": "^7.1.5",
71-
"vite-plugin-svgr": "^4.5.0"
78+
"vite-plugin-compression": "^0.5.1",
79+
"vite-plugin-imagemin": "^0.6.1",
80+
"vite-plugin-pwa": "^1.0.3",
81+
"vite-plugin-svgr": "^4.5.0",
82+
"workbox-window": "^7.3.0"
7283
}
7384
}

scripts/lighthouse.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env node
2+
3+
const { default: lighthouse } = require('lighthouse');
4+
const chromeLauncher = require('chrome-launcher');
5+
const fs = require('fs').promises;
6+
const path = require('path');
7+
8+
// Configuration
9+
const config = {
10+
extends: 'lighthouse:default',
11+
settings: {
12+
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
13+
formFactor: 'desktop',
14+
throttling: {
15+
rttMs: 40,
16+
throughputKbps: 10240,
17+
cpuSlowdownMultiplier: 1,
18+
requestLatencyMs: 0,
19+
downloadThroughputKbps: 0,
20+
uploadThroughputKbps: 0
21+
},
22+
screenEmulation: {
23+
mobile: false,
24+
width: 1920,
25+
height: 1080,
26+
deviceScaleFactor: 1,
27+
disabled: false
28+
}
29+
}
30+
};
31+
32+
async function runLighthouse(url) {
33+
let chrome;
34+
35+
try {
36+
// Launch Chrome
37+
chrome = await chromeLauncher.launch({
38+
chromeFlags: ['--headless', '--no-sandbox', '--disable-dev-shm-usage']
39+
});
40+
41+
const options = {
42+
logLevel: 'info',
43+
output: ['html', 'json'],
44+
port: chrome.port
45+
};
46+
47+
// Run Lighthouse
48+
const runnerResult = await lighthouse(url, options, config);
49+
50+
// Create reports directory
51+
const reportsDir = path.join(__dirname, '..', 'lighthouse-reports');
52+
await fs.mkdir(reportsDir, { recursive: true });
53+
54+
// Save reports
55+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
56+
const htmlPath = path.join(reportsDir, `report-${timestamp}.html`);
57+
const jsonPath = path.join(reportsDir, `report-${timestamp}.json`);
58+
59+
await fs.writeFile(htmlPath, runnerResult.report[0]);
60+
await fs.writeFile(jsonPath, runnerResult.report[1]);
61+
62+
// Parse and display scores
63+
const lhr = runnerResult.lhr;
64+
console.log('\n🚀 Lighthouse Score Report\n');
65+
console.log('=' .repeat(50));
66+
67+
const categories = lhr.categories;
68+
const scores = {
69+
'Performance': categories.performance.score * 100,
70+
'Accessibility': categories.accessibility.score * 100,
71+
'Best Practices': categories['best-practices'].score * 100,
72+
'SEO': categories.seo.score * 100
73+
};
74+
75+
Object.entries(scores).forEach(([category, score]) => {
76+
const emoji = score >= 90 ? '✅' : score >= 50 ? '⚠️' : '❌';
77+
const color = score >= 90 ? '\x1b[32m' : score >= 50 ? '\x1b[33m' : '\x1b[31m';
78+
console.log(`${emoji} ${category}: ${color}${score.toFixed(0)}%\x1b[0m`);
79+
});
80+
81+
console.log('=' .repeat(50));
82+
console.log(`\n📊 Full report saved to: ${htmlPath}`);
83+
console.log(`📈 JSON data saved to: ${jsonPath}\n`);
84+
85+
// Show top opportunities for improvement
86+
if (lhr.audits) {
87+
const opportunities = Object.values(lhr.audits)
88+
.filter(audit => audit.details?.type === 'opportunity' && audit.score < 1)
89+
.sort((a, b) => (b.details?.overallSavingsMs || 0) - (a.details?.overallSavingsMs || 0))
90+
.slice(0, 5);
91+
92+
if (opportunities.length > 0) {
93+
console.log('\n💡 Top Opportunities for Improvement:\n');
94+
opportunities.forEach((opp, idx) => {
95+
const savings = opp.details?.overallSavingsMs || 0;
96+
console.log(`${idx + 1}. ${opp.title}`);
97+
if (savings > 0) {
98+
console.log(` Potential savings: ${(savings / 1000).toFixed(2)}s`);
99+
}
100+
if (opp.description) {
101+
console.log(` ${opp.description.substring(0, 100)}...`);
102+
}
103+
console.log();
104+
});
105+
}
106+
}
107+
108+
// Show failing audits
109+
const failingAudits = Object.values(lhr.audits)
110+
.filter(audit => audit.score === 0 && audit.details?.type !== 'opportunity')
111+
.slice(0, 5);
112+
113+
if (failingAudits.length > 0) {
114+
console.log('\n⚠️ Issues to Fix:\n');
115+
failingAudits.forEach((audit, idx) => {
116+
console.log(`${idx + 1}. ${audit.title}`);
117+
if (audit.description) {
118+
console.log(` ${audit.description.substring(0, 100)}...`);
119+
}
120+
console.log();
121+
});
122+
}
123+
124+
return scores;
125+
126+
} catch (error) {
127+
console.error('Error running Lighthouse:', error);
128+
process.exit(1);
129+
} finally {
130+
if (chrome) {
131+
await chrome.kill();
132+
}
133+
}
134+
}
135+
136+
// Main execution
137+
async function main() {
138+
const url = process.argv[2] || 'http://localhost:4173/portfolio-v2';
139+
140+
console.log(`\n🔍 Running Lighthouse audit on: ${url}`);
141+
console.log('This may take a moment...\n');
142+
143+
const scores = await runLighthouse(url);
144+
145+
// Exit with error if any score is below threshold
146+
const threshold = 80;
147+
const failedCategories = Object.entries(scores)
148+
.filter(([_, score]) => score < threshold);
149+
150+
if (failedCategories.length > 0) {
151+
console.log(`\n❌ Some categories are below ${threshold}% threshold`);
152+
process.exit(1);
153+
} else {
154+
console.log(`\n✅ All categories meet the ${threshold}% threshold!`);
155+
process.exit(0);
156+
}
157+
}
158+
159+
main().catch(console.error);

scripts/optimize-images.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const sharp = require('sharp');
2+
const fs = require('fs').promises;
3+
const path = require('path');
4+
const glob = require('glob');
5+
6+
async function optimizeImages() {
7+
// Find all image files
8+
const imagePatterns = [
9+
'src/assets/images/**/*.{jpg,jpeg,png}',
10+
'public/*.{jpg,jpeg,png}'
11+
];
12+
13+
const formats = [
14+
{ format: 'webp', quality: 85 },
15+
{ format: 'avif', quality: 80 }
16+
];
17+
18+
let totalOptimized = 0;
19+
let totalSaved = 0;
20+
21+
for (const pattern of imagePatterns) {
22+
const files = glob.sync(pattern);
23+
24+
for (const file of files) {
25+
const dir = path.dirname(file);
26+
const basename = path.basename(file, path.extname(file));
27+
28+
try {
29+
const originalStats = await fs.stat(file);
30+
const originalSize = originalStats.size;
31+
32+
// Create optimized versions
33+
for (const { format, quality } of formats) {
34+
const outputPath = path.join(dir, `${basename}.${format}`);
35+
36+
// Skip if already exists
37+
try {
38+
await fs.access(outputPath);
39+
console.log(`✓ ${outputPath} already exists, skipping...`);
40+
continue;
41+
} catch {
42+
// File doesn't exist, proceed with conversion
43+
}
44+
45+
await sharp(file)
46+
[format]({ quality })
47+
.toFile(outputPath);
48+
49+
const newStats = await fs.stat(outputPath);
50+
const saved = originalSize - newStats.size;
51+
totalSaved += saved;
52+
53+
console.log(`✓ Created ${outputPath} (saved ${(saved / 1024).toFixed(1)}KB)`);
54+
totalOptimized++;
55+
}
56+
} catch (error) {
57+
console.error(`✗ Error processing ${file}:`, error.message);
58+
}
59+
}
60+
}
61+
62+
console.log(`\n🎉 Optimization complete!`);
63+
console.log(` Converted ${totalOptimized} images`);
64+
console.log(` Total space saved: ${(totalSaved / 1024 / 1024).toFixed(2)}MB`);
65+
}
66+
67+
optimizeImages().catch(console.error);

src/App.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import "./App.css";
22
import "./styles/accessibility.css";
3-
import Content from "./components/Content";
4-
import Footer from "./components/Footer";
3+
import { lazy, Suspense, useEffect } from "react";
54
import Header from "./components/Header";
65
import { ScrollProgress } from "./components/ScrollProgress";
76
import { SkipLink } from "./components/SkipLink";
87
import { AudioProvider } from "./contexts/AudioContext";
98
import { ThemeProvider } from "./contexts/ThemeContext";
109
import "./i18n";
11-
import { useEffect } from "react";
1210
import { logPerformanceReport } from "./utils/performanceProfiler";
1311

12+
// Lazy load components for better performance
13+
const Content = lazy(() => import("./components/Content"));
14+
const Footer = lazy(() => import("./components/Footer"));
15+
1416
function App() {
1517
useEffect(() => {
1618
// Add performance report button in development
@@ -46,8 +48,14 @@ function App() {
4648
<SkipLink />
4749
<ScrollProgress />
4850
<Header />
49-
<Content />
50-
<Footer />
51+
<Suspense fallback={
52+
<div className="flex justify-center items-center min-h-screen">
53+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
54+
</div>
55+
}>
56+
<Content />
57+
<Footer />
58+
</Suspense>
5159
</AudioProvider>
5260
</ThemeProvider>
5361
);

src/assets/images/haeundae.avif

431 KB
Binary file not shown.

src/assets/images/haeundae.webp

413 KB
Loading

src/assets/images/hanra.avif

763 KB
Binary file not shown.

src/assets/images/hanra.webp

677 KB
Loading

0 commit comments

Comments
 (0)