Skip to content

Commit e6ba2ff

Browse files
committed
chore: perf improvements and blog post
1 parent 4d51d8d commit e6ba2ff

43 files changed

Lines changed: 989 additions & 390 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

animated-number-implementations.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# AnimatedNumber Implementations
2+
3+
| File | Line(s) | Usage Context | Value Type | Props |
4+
|------|---------|---------------|------------|-------|
5+
| **src/app/Test.tsx** | 265-270 | SectionHeading right text | `rightText` (number) | `duration`, `initialProgress={0}`, `className` |
6+
| **src/components/blog/post-view.tsx** | 92 | Blog post date day | `dateParts.day` | `duration={500}`, `initialProgress={0}` |
7+
| | 92 | Blog post date year | `dateParts.year` | `duration={500}`, `initialProgress={0}` |
8+
| | 96 | Reading time minutes | `readTimeMinutes` | `duration={500}`, `initialProgress={0}` |
9+
| | 102 | Unique views count | `uniqueViews` | `duration={500}`, `initialProgress={0}` |
10+
| **src/components/blog/posts-client.tsx** | 15 | Post count header | `count` | `duration={500}`, `initialProgress={0}` |
11+
| | 92 | Blog card index number | `formattedIndex` | `duration={1800-2400}` (staggered) |
12+
| | 123 | Extra tags count | `+${extraTags}` | `duration={1800-2400}`, `initialProgress={0}` |
13+
| | 131 | Blog card date day | `dayNumber` | `duration={1400-1800}` |
14+
| | 138-140 | Reading time minutes | `readTimeMinutes` | `duration={1400-1800}` |
15+
| | 148-151 | Unique views count | `post.uniqueViews` | `duration={1400-1800}` |
16+
| **src/components/home/hero.tsx** | 30 | Years of experience | `8` | `duration={1500}`, `immediate`, `initialProgress={0.6}` |
17+
| **src/components/landing/activity/contribution-graph.tsx** | 418 | Total contributions | `totalContributions.toLocaleString()` | `duration={2000}`, `delay={0}`, `animateOnMount` |
18+
| | 418 | Year display | `year` | `duration={1800}`, `delay={200}`, `animateOnMount` |
19+
| **src/components/landing/activity/section.tsx** | 27 | Activity section year | `year` | `duration={600}`, `delay={200}`, `initialProgress={0}` |
20+
| **src/components/layout/footer.tsx** | 83 | Last commit time ago | `relativeTimeInfo.value` | `duration={600}`, `initialProgress={0}`, `className` |
21+
| **src/components/providers/providers.tsx** | 11, 25 | Provider wrapper | N/A (Provider component) | N/A |
22+
| **src/components/ui/work-experience.tsx** | 225 | Employment period year | `year` | `initialProgress={0}` |
23+
| | 287 | Employment period year | `year` | `initialProgress={0}` |
24+
25+
## Summary:
26+
- **Total files using AnimatedNumber**: 9 files
27+
- **Total instances**: 16 usage instances
28+
- **Common patterns**: Dates (years, days), statistics (views, counts), time-based values
29+
- **Most common props**: `initialProgress={0}`, `duration` (varies 500-2400ms), `className` for styling
30+
- **Special cases**: Provider wrapper, formatted strings with locale, staggered animations in blog cards

package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@
1313
"engines": {
1414
"node": "24.x"
1515
},
16-
"overrides": {
17-
"better-auth": {
18-
"drizzle-orm": "$drizzle-orm"
19-
}
20-
},
2116
"dependencies": {
2217
"@neondatabase/serverless": "^1.0.2",
2318
"@radix-ui/react-collapsible": "^1.1.12",

scripts/measure-vitals.sh

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/bin/bash
2+
3+
# Web Vitals Measurement Script using Lighthouse
4+
# Usage: ./scripts/measure-vitals.sh [url]
5+
# Examples:
6+
# ./scripts/measure-vitals.sh # Measure localhost:3000
7+
# ./scripts/measure-vitals.sh https://site.com # Measure external URL
8+
9+
set -e
10+
11+
URL="${1:-http://localhost:3000}"
12+
13+
echo "📊 Measuring Web Vitals for: $URL"
14+
echo ""
15+
16+
# Check if server is reachable
17+
if ! curl -s -o /dev/null -w "%{http_code}" "$URL" | grep -q "200\|301\|302"; then
18+
echo "❌ Cannot reach $URL - make sure your server is running"
19+
exit 1
20+
fi
21+
22+
echo "✅ Server is reachable"
23+
echo ""
24+
25+
# Run Lighthouse CLI (it installs automatically via npx)
26+
npx -y lighthouse "$URL" \
27+
--only-categories=performance \
28+
--output=json \
29+
--output-path=/tmp/lighthouse-report.json \
30+
--chrome-flags="--headless --no-sandbox" \
31+
--quiet
32+
33+
# Parse and display results
34+
node -e "
35+
const fs = require('fs');
36+
const report = JSON.parse(fs.readFileSync('/tmp/lighthouse-report.json', 'utf8'));
37+
const audits = report.audits;
38+
39+
const metrics = {
40+
'LCP': { value: audits['largest-contentful-paint']?.numericValue / 1000, unit: 's', good: 2.5, fair: 4.0 },
41+
'FCP': { value: audits['first-contentful-paint']?.numericValue / 1000, unit: 's', good: 1.8, fair: 3.0 },
42+
'TBT': { value: audits['total-blocking-time']?.numericValue / 1000, unit: 's', good: 0.2, fair: 0.6 },
43+
'CLS': { value: audits['cumulative-layout-shift']?.numericValue, unit: '', good: 0.1, fair: 0.25 },
44+
'TTI': { value: audits['interactive']?.numericValue / 1000, unit: 's', good: 3.8, fair: 7.3 },
45+
'Speed Index': { value: audits['speed-index']?.numericValue / 1000, unit: 's', good: 3.4, fair: 5.8 },
46+
};
47+
48+
const getStatus = (value, good, fair) => {
49+
if (value <= good) return '🟢 Good';
50+
if (value <= fair) return '🟡 Needs Improvement';
51+
return '🔴 Critical';
52+
};
53+
54+
const getStatusText = (value, good, fair) => {
55+
if (value <= good) return 'Good';
56+
if (value <= fair) return 'Needs Improvement';
57+
return 'Critical';
58+
};
59+
60+
console.log('📊 Web Vitals Results');
61+
console.log('─'.repeat(60));
62+
63+
Object.entries(metrics).forEach(([name, data]) => {
64+
if (data.value !== undefined) {
65+
const formatted = data.unit === 's' ? data.value.toFixed(2) + 's' : data.value.toFixed(3);
66+
console.log(\`\${name.padEnd(12)} │ \${formatted.padEnd(8)} │ \${getStatus(data.value, data.good, data.fair)}\`);
67+
}
68+
});
69+
70+
console.log('─'.repeat(60));
71+
console.log('Performance Score:', Math.round(report.categories.performance.score * 100) + '/100');
72+
console.log('');
73+
74+
// Markdown table
75+
console.log('📝 Markdown Table:');
76+
console.log('');
77+
console.log('| Metric | Measured Value | Status |');
78+
console.log('| :--- | :--- | :--- |');
79+
80+
const metricNames = {
81+
'LCP': 'LCP (Largest Contentful Paint)',
82+
'FCP': 'FCP (First Contentful Paint)',
83+
'TBT': 'TBT (Total Blocking Time)',
84+
'CLS': 'CLS (Cumulative Layout Shift)',
85+
};
86+
87+
['LCP', 'FCP', 'TBT', 'CLS'].forEach(key => {
88+
const data = metrics[key];
89+
if (data.value !== undefined) {
90+
const formatted = data.unit === 's' ? data.value.toFixed(2) + 's' : data.value.toFixed(3);
91+
console.log(\`| **\${metricNames[key]}** | \${formatted} | \${getStatusText(data.value, data.good, data.fair)} |\`);
92+
}
93+
});
94+
"
95+
96+
# Cleanup
97+
rm -f /tmp/lighthouse-report.json
98+
99+
echo ""
100+
echo "✅ Done!"

scripts/measure-web-vitals.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Script to measure Web Vitals for your site
2+
// Run this in the browser console or integrate into your app
3+
4+
class WebVitalsMonitor {
5+
constructor() {
6+
this.metrics = {
7+
LCP: null,
8+
FCP: null,
9+
TTFB: null,
10+
TBT: null,
11+
CLS: null,
12+
};
13+
this.tbtData = {
14+
fcpTime: 0,
15+
longTasks: [],
16+
};
17+
}
18+
19+
// Measure Largest Contentful Paint
20+
measureLCP() {
21+
return new Promise((resolve) => {
22+
const observer = new PerformanceObserver((list) => {
23+
const entries = list.getEntries();
24+
const lastEntry = entries[entries.length - 1];
25+
this.metrics.LCP = (lastEntry.renderTime || lastEntry.loadTime) / 1000;
26+
console.log('✅ LCP:', this.metrics.LCP.toFixed(2) + 's');
27+
});
28+
observer.observe({ type: 'largest-contentful-paint', buffered: true });
29+
30+
// LCP can change, so we'll resolve after a timeout
31+
setTimeout(() => resolve(this.metrics.LCP), 5000);
32+
});
33+
}
34+
35+
// Measure First Contentful Paint
36+
measureFCP() {
37+
return new Promise((resolve) => {
38+
const observer = new PerformanceObserver((list) => {
39+
const entries = list.getEntries();
40+
entries.forEach((entry) => {
41+
if (entry.name === 'first-contentful-paint') {
42+
this.metrics.FCP = entry.startTime / 1000;
43+
this.tbtData.fcpTime = entry.startTime;
44+
console.log('✅ FCP:', this.metrics.FCP.toFixed(2) + 's');
45+
resolve(this.metrics.FCP);
46+
}
47+
});
48+
});
49+
observer.observe({ type: 'paint', buffered: true });
50+
});
51+
}
52+
53+
// Measure Time to First Byte
54+
measureTTFB() {
55+
const navEntry = performance.getEntriesByType('navigation')[0];
56+
if (navEntry) {
57+
this.metrics.TTFB = navEntry.responseStart / 1000;
58+
console.log('✅ TTFB:', this.metrics.TTFB.toFixed(2) + 's');
59+
return this.metrics.TTFB;
60+
}
61+
return null;
62+
}
63+
64+
// Measure Total Blocking Time
65+
measureTBT() {
66+
return new Promise((resolve) => {
67+
const observer = new PerformanceObserver((list) => {
68+
const entries = list.getEntries();
69+
entries.forEach((entry) => {
70+
if (entry.startTime >= this.tbtData.fcpTime) {
71+
this.tbtData.longTasks.push({
72+
duration: entry.duration,
73+
startTime: entry.startTime,
74+
});
75+
}
76+
});
77+
});
78+
observer.observe({ type: 'longtask', buffered: true });
79+
80+
// Calculate TBT after page is interactive
81+
setTimeout(() => {
82+
let totalBlockingTime = 0;
83+
this.tbtData.longTasks.forEach((task) => {
84+
if (task.duration > 50) {
85+
totalBlockingTime += task.duration - 50;
86+
}
87+
});
88+
this.metrics.TBT = totalBlockingTime / 1000;
89+
console.log('✅ TBT:', this.metrics.TBT.toFixed(2) + 's');
90+
console.log(' Long tasks:', this.tbtData.longTasks.length);
91+
resolve(this.metrics.TBT);
92+
}, 5000);
93+
});
94+
}
95+
96+
// Measure Cumulative Layout Shift
97+
measureCLS() {
98+
return new Promise((resolve) => {
99+
let clsValue = 0;
100+
const observer = new PerformanceObserver((list) => {
101+
const entries = list.getEntries();
102+
entries.forEach((entry) => {
103+
// Only count layout shifts without recent user input
104+
if (!entry.hadRecentInput) {
105+
clsValue += entry.value;
106+
}
107+
});
108+
this.metrics.CLS = clsValue;
109+
});
110+
observer.observe({ type: 'layout-shift', buffered: true });
111+
112+
// Observe for a while, then resolve
113+
setTimeout(() => {
114+
console.log('✅ CLS:', this.metrics.CLS.toFixed(3));
115+
resolve(this.metrics.CLS);
116+
}, 5000);
117+
});
118+
}
119+
120+
// Get status for each metric
121+
getStatus(metric, value) {
122+
const thresholds = {
123+
LCP: { good: 2.5, needsImprovement: 4.0 },
124+
FCP: { good: 1.8, needsImprovement: 3.0 },
125+
TTFB: { good: 0.8, needsImprovement: 1.8 },
126+
TBT: { good: 0.2, needsImprovement: 0.6 },
127+
CLS: { good: 0.1, needsImprovement: 0.25 },
128+
};
129+
130+
const t = thresholds[metric];
131+
if (value <= t.good) return '🟢 Good';
132+
if (value <= t.needsImprovement) return '🟡 Needs Improvement';
133+
return '🔴 Critical';
134+
}
135+
136+
// Run all measurements
137+
async measureAll() {
138+
console.log('🚀 Starting Web Vitals measurement...\n');
139+
140+
// Measure TTFB immediately
141+
this.measureTTFB();
142+
143+
// Wait for paint metrics
144+
await this.measureFCP();
145+
146+
// Run LCP, TBT, and CLS in parallel
147+
await Promise.all([
148+
this.measureLCP(),
149+
this.measureTBT(),
150+
this.measureCLS(),
151+
]);
152+
153+
this.displayResults();
154+
return this.metrics;
155+
}
156+
157+
// Display formatted results
158+
displayResults() {
159+
console.log('\n📊 Web Vitals Results:');
160+
console.log('━'.repeat(60));
161+
162+
Object.entries(this.metrics).forEach(([metric, value]) => {
163+
if (value !== null) {
164+
const status = this.getStatus(metric, value);
165+
const unit = metric === 'CLS' ? '' : 's';
166+
console.log(`${metric.padEnd(6)} | ${value.toFixed(metric === 'CLS' ? 3 : 2)}${unit.padEnd(2)} | ${status}`);
167+
}
168+
});
169+
170+
console.log('━'.repeat(60));
171+
}
172+
173+
// Export as markdown table
174+
exportAsMarkdown() {
175+
let markdown = '| Metric | Measured Value | Status |\n';
176+
markdown += '| :--- | :--- | :--- |\n';
177+
178+
const metricNames = {
179+
LCP: 'LCP (Largest Contentful Paint)',
180+
FCP: 'FCP (First Contentful Paint)',
181+
TTFB: 'TTFB (Time to First Byte)',
182+
TBT: 'TBT (Total Blocking Time)',
183+
CLS: 'CLS (Cumulative Layout Shift)',
184+
};
185+
186+
Object.entries(this.metrics).forEach(([metric, value]) => {
187+
if (value !== null) {
188+
const name = metricNames[metric];
189+
const unit = metric === 'CLS' ? '' : 's';
190+
const status = this.getStatus(metric, value).replace(/[🟢🟡🔴] /, '');
191+
markdown += `| **${name}** | ${value.toFixed(metric === 'CLS' ? 3 : 2)}${unit} | ${status} |\n`;
192+
}
193+
});
194+
195+
console.log('\n📝 Markdown Table:\n');
196+
console.log(markdown);
197+
return markdown;
198+
}
199+
}
200+
201+
// Usage:
202+
// const monitor = new WebVitalsMonitor();
203+
// monitor.measureAll().then(metrics => {
204+
// monitor.exportAsMarkdown();
205+
// });
206+
207+
// Auto-run if in browser
208+
if (typeof window !== 'undefined') {
209+
window.WebVitalsMonitor = WebVitalsMonitor;
210+
console.log('💡 Usage: const monitor = new WebVitalsMonitor(); await monitor.measureAll();');
211+
}

src/app/(marketing)/blog/[...slug]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { blogPosts } from '@/server/db/schema'
1414
import { eq } from 'drizzle-orm'
1515

1616
// Force dynamic rendering due to auth requirements
17+
// Must be dynamic due to auth (cookies/headers) usage
1718
export const dynamic = 'force-dynamic'
1819

1920
export async function generateStaticParams() {

src/app/(marketing)/blog/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { BlogPosts } from '@/components/blog/posts'
22
import { TopicsSidebar } from '@/components/blog/topics-sidebar'
33

4-
export const dynamic = 'force-dynamic'
4+
// Enable ISR - revalidate every 60 seconds
5+
export const revalidate = 60
56

67
export const metadata = {
78
title: 'Blog',

0 commit comments

Comments
 (0)