Skip to content

Commit d6f506a

Browse files
committed
perf
1 parent 2a10dee commit d6f506a

File tree

6 files changed

+67
-132
lines changed

6 files changed

+67
-132
lines changed

src/components/Markdown.tsx

Lines changed: 21 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useToast } from '~/components/ToastProvider'
1414
import { twMerge } from 'tailwind-merge'
1515
import { useMarkdownHeadings } from '~/components/MarkdownHeadingContext'
1616
import { renderMarkdown } from '~/utils/markdown'
17+
import { getNetlifyImageUrl } from '~/utils/netlifyImage'
1718
import { Tabs } from '~/components/Tabs'
1819
import { Copy } from 'lucide-react'
1920

@@ -82,7 +83,7 @@ const markdownComponents: Record<string, React.FC> = {
8283
return (
8384
<span
8485
className={`border border-gray-500/20 bg-gray-500/10 rounded px-1 py-0.5${
85-
className ?? ` ${className}`
86+
className ? ` ${className}` : ''
8687
}`}
8788
{...rest}
8889
/>
@@ -91,30 +92,16 @@ const markdownComponents: Record<string, React.FC> = {
9192
iframe: (props) => (
9293
<iframe {...props} className="w-full" title="Embedded Content" />
9394
),
94-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
95-
img: ({ children, alt, src, ...props }: HTMLProps<HTMLImageElement>) => {
96-
// Use Netlify Image CDN for local images
97-
const optimizedSrc =
98-
src &&
99-
!src.startsWith('http') &&
100-
!src.startsWith('data:') &&
101-
!src.endsWith('.svg')
102-
? `/.netlify/images?url=${encodeURIComponent(src)}&w=800&q=80`
103-
: src
104-
105-
return (
106-
<img
107-
{...props}
108-
src={optimizedSrc}
109-
alt={alt ?? ''}
110-
className={`max-w-full h-auto rounded-lg shadow-md ${
111-
props.className ?? ''
112-
}`}
113-
loading="lazy"
114-
decoding="async"
115-
/>
116-
)
117-
},
95+
img: ({ alt, src, className, children: _, ...props }: HTMLProps<HTMLImageElement>) => (
96+
<img
97+
{...props}
98+
src={src ? getNetlifyImageUrl(src) : undefined}
99+
alt={alt ?? ''}
100+
className={`max-w-full h-auto rounded-lg shadow-md ${className ?? ''}`}
101+
loading="lazy"
102+
decoding="async"
103+
/>
104+
),
118105
}
119106

120107
export function extractPreAttributes(html: string): {
@@ -194,13 +181,10 @@ export function CodeBlock({
194181
](() => {
195182
;(async () => {
196183
const themes = ['github-light', 'tokyo-night']
197-
198-
// Normalize language name
199184
const normalizedLang = LANG_ALIASES[lang] || lang
200-
const effectiveLang =
201-
normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
185+
const effectiveLang = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
202186

203-
const highlighter = await getHighlighter(lang, themes)
187+
const highlighter = await getHighlighter(lang)
204188

205189
const htmls = await Promise.all(
206190
themes.map(async (theme) => {
@@ -292,33 +276,6 @@ export function CodeBlock({
292276
)
293277
}
294278

295-
const cache = <T extends (...args: any[]) => any>(fn: T) => {
296-
const cache = new Map<string, any>()
297-
return async (...args: Parameters<T>) => {
298-
const key = JSON.stringify(args)
299-
if (cache.has(key)) {
300-
return cache.get(key)
301-
}
302-
const value = await fn(...args)
303-
cache.set(key, value)
304-
return value
305-
}
306-
}
307-
308-
// Core languages to bundle (most commonly used in TanStack docs)
309-
const CORE_LANGS = [
310-
'typescript',
311-
'javascript',
312-
'tsx',
313-
'jsx',
314-
'bash',
315-
'json',
316-
'html',
317-
'css',
318-
'markdown',
319-
'plaintext',
320-
] as const
321-
322279
// Language aliases mapping
323280
const LANG_ALIASES: Record<string, string> = {
324281
ts: 'typescript',
@@ -332,41 +289,32 @@ const LANG_ALIASES: Record<string, string> = {
332289
text: 'plaintext',
333290
}
334291

335-
// Lazy highlighter initialization
292+
// Lazy highlighter singleton
336293
let highlighterPromise: Promise<HighlighterGeneric<any, any>> | null = null
337294

338-
async function getShikiHighlighter() {
295+
async function getHighlighter(language: string) {
339296
if (!highlighterPromise) {
340297
highlighterPromise = createHighlighter({
341298
themes: ['github-light', 'tokyo-night'],
342-
langs: CORE_LANGS as unknown as string[],
299+
langs: ['typescript', 'javascript', 'tsx', 'jsx', 'bash', 'json', 'html', 'css', 'markdown', 'plaintext'],
343300
})
344301
}
345-
return highlighterPromise
346-
}
347-
348-
const getHighlighter = cache(async (language: string, _themes: string[]) => {
349-
const highlighter = await getShikiHighlighter()
350302

351-
// Normalize language name
303+
const highlighter = await highlighterPromise
352304
const normalizedLang = LANG_ALIASES[language] || language
353305
const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
354306

355-
const loadedLanguages = highlighter.getLoadedLanguages()
356-
357-
// Only load language if not already loaded
358-
if (!loadedLanguages.includes(langToLoad as any)) {
307+
// Load language if not already loaded
308+
if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
359309
try {
360-
// Load language using shiki's built-in loader (works with bundle/web)
361310
await highlighter.loadLanguage(langToLoad as any)
362311
} catch {
363-
// Fallback to plaintext if language not found
364312
console.warn(`Shiki: Language "${langToLoad}" not found, using plaintext`)
365313
}
366314
}
367315

368316
return highlighter
369-
})
317+
}
370318

371319
const options: HTMLReactParserOptions = {
372320
replace: (domNode) => {

src/components/Navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ export function Navbar({ children }: { children: React.ReactNode }) {
448448
label: (
449449
<>
450450
<span>Feed</span>
451-
<span className="px-1.5 py-0.5 text-[.6rem] font-black bg-gradient-to-r from-be-400 to-be-600 text-white rounded-md uppercase">
451+
<span className="px-1.5 py-0.5 text-[.6rem] font-black bg-gradient-to-r from-blue-400 to-blue-600 text-white rounded-md uppercase">
452452
Beta
453453
</span>
454454
</>

src/components/NetlifyImage.tsx

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react'
2+
import { getNetlifyImageUrl } from '~/utils/netlifyImage'
23

34
type NetlifyImageProps = Omit<
45
React.ImgHTMLAttributes<HTMLImageElement>,
@@ -8,56 +9,26 @@ type NetlifyImageProps = Omit<
89
width?: number
910
height?: number
1011
quality?: number
11-
format?: 'avif' | 'webp' | 'jpg' | 'png' | 'gif'
12-
fit?: 'contain' | 'cover' | 'fill'
13-
position?: 'top' | 'bottom' | 'left' | 'right' | 'center'
1412
}
1513

1614
/**
1715
* Optimized image component using Netlify Image CDN.
18-
* Automatically transforms images on-demand with:
19-
* - Format conversion (auto webp/avif based on browser support)
20-
* - Resizing to specified dimensions
21-
* - Quality optimization
22-
* - Edge caching
23-
*
2416
* @see https://docs.netlify.com/build/image-cdn/overview/
2517
*/
2618
export function NetlifyImage({
2719
src,
2820
width,
2921
height,
30-
quality = 75,
31-
format,
32-
fit,
33-
position,
22+
quality,
3423
alt = '',
3524
loading = 'lazy',
3625
decoding = 'async',
3726
...props
3827
}: NetlifyImageProps) {
39-
const optimizedSrc = React.useMemo(() => {
40-
// Skip optimization for external URLs, data URIs, or SVGs
41-
if (
42-
src.startsWith('http') ||
43-
src.startsWith('data:') ||
44-
src.endsWith('.svg')
45-
) {
46-
return src
47-
}
48-
49-
const params = new URLSearchParams()
50-
params.set('url', src)
51-
52-
if (width) params.set('w', String(width))
53-
if (height) params.set('h', String(height))
54-
if (quality !== 75) params.set('q', String(quality))
55-
if (format) params.set('fm', format)
56-
if (fit) params.set('fit', fit)
57-
if (position) params.set('position', position)
58-
59-
return `/.netlify/images?${params.toString()}`
60-
}, [src, width, height, quality, format, fit, position])
28+
const optimizedSrc = React.useMemo(
29+
() => getNetlifyImageUrl(src, { width, height, quality }),
30+
[src, width, height, quality],
31+
)
6132

6233
return (
6334
<img

src/components/SimpleMarkdown.tsx

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import parse, {
88
HTMLReactParserOptions,
99
} from 'html-react-parser'
1010
import { renderMarkdown } from '~/utils/markdown'
11+
import { getNetlifyImageUrl } from '~/utils/netlifyImage'
1112

1213
/**
1314
* Lightweight markdown renderer for simple content like excerpts.
@@ -37,27 +38,16 @@ const markdownComponents: Record<string, React.FC<any>> = {
3738
</pre>
3839
)
3940
},
40-
img: ({ children, alt, src, ...props }: HTMLProps<HTMLImageElement>) => {
41-
// Use Netlify Image CDN for local images
42-
const optimizedSrc =
43-
src &&
44-
!src.startsWith('http') &&
45-
!src.startsWith('data:') &&
46-
!src.endsWith('.svg')
47-
? `/.netlify/images?url=${encodeURIComponent(src)}&w=800&q=80`
48-
: src
49-
50-
return (
51-
<img
52-
{...props}
53-
src={optimizedSrc}
54-
alt={alt ?? ''}
55-
className={`max-w-full h-auto rounded-lg shadow-md ${props.className ?? ''}`}
56-
loading="lazy"
57-
decoding="async"
58-
/>
59-
)
60-
},
41+
img: ({ alt, src, className, children: _, ...props }: HTMLProps<HTMLImageElement>) => (
42+
<img
43+
{...props}
44+
src={src ? getNetlifyImageUrl(src) : undefined}
45+
alt={alt ?? ''}
46+
className={`max-w-full h-auto rounded-lg shadow-md ${className ?? ''}`}
47+
loading="lazy"
48+
decoding="async"
49+
/>
50+
),
6151
}
6252

6353
const options: HTMLReactParserOptions = {

src/hooks/useNpmDownloadCounter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export function useNpmDownloadCounter(
2929
// Calculate how often we need to update based on rate
3030
// At minimum, update when count would change by 1
3131
const msPerIncrement = 1 / ratePerMs
32-
// Clamp between 50ms and 1000ms
33-
const intervalMs = Math.max(50, Math.min(1000, msPerIncrement))
32+
// Clamp between 16ms (60fps) and 1000ms
33+
const intervalMs = Math.max(16, Math.min(1000, msPerIncrement))
3434

3535
const tick = () => {
3636
if (!elementRef.current) return

src/utils/netlifyImage.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Build a Netlify Image CDN URL for optimized image delivery.
3+
* Returns the original src in development or for external URLs, data URIs, and SVGs.
4+
*
5+
* @see https://docs.netlify.com/build/image-cdn/overview/
6+
*/
7+
export function getNetlifyImageUrl(
8+
src: string,
9+
options: { width?: number; height?: number; quality?: number } = {},
10+
): string {
11+
// Skip in development - Netlify Image CDN only works in production
12+
if (import.meta.env.DEV) {
13+
return src
14+
}
15+
16+
if (
17+
src.startsWith('http') ||
18+
src.startsWith('data:') ||
19+
src.endsWith('.svg')
20+
) {
21+
return src
22+
}
23+
24+
const { width = 800, quality = 80 } = options
25+
return `/.netlify/images?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`
26+
}

0 commit comments

Comments
 (0)