Skip to content

Commit 61ec3f9

Browse files
authored
Optimize GitHub Avatars with Next.js Image Component (#8184)
## Description GitHub avatar images across 7 components were rendered using raw HTML <img> tags, bypassing Next.js built-in image optimization. This resulted in: No automatic WebP/AVIF format conversion No lazy loading out of the box No responsive sizing Higher bandwidth usage and slower LCP (Largest Contentful Paint) scores The next.config.ts already had both avatars.githubusercontent.com and github.com configured under remotePatterns, meaning the optimization infrastructure was ready — just not being used. Every GitHub avatar across the dashboard, leaderboard, burnout analyzer, and developer arena will now be: Served as WebP/AVIF automatically where the browser supports it Lazy loaded by default (images below the fold don't block rendering) Properly sized to avoid layout shift (explicit width/height on every <Image>) Fixes #3696 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [x] 📐 Pillar 2 — Geometric SVG Improvement - [x] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents ded12c8 + 562b334 commit 61ec3f9

7 files changed

Lines changed: 33 additions & 26 deletions

File tree

components/DeveloperArena.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,9 +522,11 @@ export default function DeveloperArena({ onSelectBattle }: DeveloperArenaProps)
522522
className="flex items-start gap-4"
523523
>
524524
<div className="relative w-16 h-16 rounded-xl overflow-hidden border border-white/10 shrink-0">
525-
<img
525+
<Image
526526
src={`https://github.com/${currentChallenge.username}.png`}
527527
alt={currentChallenge.name}
528+
width={64}
529+
height={64}
528530
className="w-full h-full object-cover"
529531
/>
530532
</div>
@@ -723,9 +725,11 @@ export default function DeveloperArena({ onSelectBattle }: DeveloperArenaProps)
723725
</div>
724726
<div className="flex items-center gap-3">
725727
<div className="relative w-12 h-12 rounded-full overflow-hidden border border-white/10 bg-zinc-800 shrink-0">
726-
<img
728+
<Image
727729
src={`https://github.com/${legend.username}.png`}
728730
alt={legend.name}
731+
width={48}
732+
height={48}
729733
className="w-full h-full object-cover"
730734
/>
731735
</div>

components/Leaderboard.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ export default function Leaderboard({ contributors = [] }: LeaderboardProps) {
108108
{/* Avatar */}
109109
<div className="relative w-10 h-10 rounded-full overflow-hidden border border-black/10 dark:border-white/10 group-hover:border-cyan-400/40 transition-colors">
110110
{contributor.avatar_url ? (
111-
<img
111+
<Image
112112
src={contributor.avatar_url}
113113
alt={contributor.login}
114+
width={40}
115+
height={40}
114116
className="w-full h-full object-cover"
115117
/>
116118
) : (
@@ -222,9 +224,11 @@ function PodiumItem({ contributor, rank, height, variant, delay, isFirst }: Podi
222224
${isFirst ? 'w-16 h-16 sm:w-[88px] sm:h-[88px]' : 'w-12 h-12 sm:w-[72px] sm:h-[72px]'}`}
223225
>
224226
{contributor.avatar_url ? (
225-
<img
227+
<Image
226228
src={contributor.avatar_url}
227229
alt={contributor.login}
230+
width={88}
231+
height={88}
228232
className="rounded-full object-cover w-full h-full"
229233
/>
230234
) : (

components/burnout/BurnoutRiskTable.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,11 @@ export default function BurnoutRiskTable({ contributors }: BurnoutRiskTableProps
128128
className="flex items-center gap-3 cursor-pointer"
129129
>
130130
<div className="relative w-8 h-8 rounded-full overflow-hidden border border-black/10 dark:border-white/10">
131-
<img
131+
<Image
132132
src={c.avatarUrl}
133133
alt={c.username}
134+
width={32}
135+
height={32}
134136
className="w-full h-full object-cover"
135137
/>
136138
</div>

components/burnout/InactivityDetector.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ export default function InactivityDetector({ alerts }: InactivityDetectorProps)
7575
rel="noopener noreferrer"
7676
className="relative w-9 h-9 rounded-full overflow-hidden border border-black/10 dark:border-white/10 shrink-0 cursor-pointer"
7777
>
78-
<img
78+
<Image
7979
src={alert.avatarUrl}
8080
alt={alert.username}
81+
width={36}
82+
height={36}
8183
className="w-full h-full object-cover"
8284
/>
8385
</a>

components/dashboard/GithubWrapped.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ export default function GithubWrapped({ profile, wrappedData }: GithubWrappedPro
5050
{/* Header */}
5151
<motion.div variants={itemVariants} className="flex items-center justify-between">
5252
<div className="flex items-center gap-4">
53-
<img
53+
<Image
5454
src={
5555
profile.avatarUrl.startsWith('http') || profile.avatarUrl.startsWith('/')
5656
? profile.avatarUrl
5757
: `/${profile.avatarUrl}`
5858
}
5959
alt={profile.name || 'GitHub profile avatar'}
60-
width="64"
61-
height="64"
60+
width={64}
61+
height={64}
6262
className="w-16 h-16 rounded-full border-2 border-white/20 object-cover"
6363
/>
6464
<div>

components/dashboard/ProfileCard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,15 @@ export default function ProfileCard({ user, exportData, badges }: ProfileCardPro
7878
<div className="flex flex-col items-center text-center">
7979
<div className="relative mb-5">
8080
<div className="w-24 h-24 rounded-full overflow-hidden border border-black/10 dark:border-[rgba(255,255,255,0.12)]">
81-
<img
81+
<Image
8282
src={
8383
user.avatarUrl.startsWith('http')
8484
? `${user.avatarUrl}${user.avatarUrl.includes('?') ? '&' : '?'}s=120`
8585
: user.avatarUrl
8686
}
8787
alt={user.name || 'Contributor Avatar'}
88-
width="96"
89-
height="96"
88+
width={96}
89+
height={96}
9090
className="w-full h-full aspect-square object-cover"
9191
/>
9292
</div>

components/dashboard/ShareSheet.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { copyToClipboard } from '@/utils/clipboard';
4+
import Image from 'next/image';
45
import {
56
useEffect,
67
useRef,
@@ -111,23 +112,17 @@ const WhatsAppIcon = ({ size = 15 }: { size?: number }) => (
111112
);
112113

113114
function GitHubAvatar({ username }: { username: string }) {
114-
const [src, setSrc] = useState<string | null>(null);
115+
const [errored, setErrored] = useState(false);
115116

116-
useEffect(() => {
117-
const img = new Image();
118-
img.onload = () => setSrc(`https://avatars.githubusercontent.com/${username}?size=64`);
119-
img.onerror = () => setSrc(null);
120-
img.src = `https://avatars.githubusercontent.com/${username}?size=64`;
121-
}, [username]);
122-
123-
if (src) {
117+
if (!errored) {
124118
return (
125-
<img
126-
src={src}
119+
<Image
120+
src={`https://avatars.githubusercontent.com/${username}?size=64`}
127121
alt={username}
128-
width="36"
129-
height="36"
122+
width={36}
123+
height={36}
130124
className="w-9 h-9 rounded-full ring-2 ring-zinc-200 dark:ring-zinc-700 object-cover shrink-0"
125+
onError={() => setErrored(true)}
131126
/>
132127
);
133128
}
@@ -261,7 +256,7 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
261256
try {
262257
const svgString = new XMLSerializer().serializeToString(svgElement);
263258
const blobURL = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' }));
264-
const image = new Image();
259+
const image = new window.Image();
265260
image.onload = async () => {
266261
const canvas = document.createElement('canvas');
267262
canvas.width = 512;

0 commit comments

Comments
 (0)