@@ -15,6 +15,80 @@ import { parseSearchQuery } from "@web-speed-hackathon-2026/server/src/utils/par
1515
1616export const staticRouter = Router ( ) ;
1717
18+ /** HTML 埋め込み用: 説明文など巨大フィールドを除き、インラインスクリプトのパース時間を抑える */
19+ function trimUserForClientPreload ( u : unknown ) : Record < string , unknown > | null {
20+ if ( u == null || typeof u !== "object" ) return null ;
21+ const raw = u as Record < string , unknown > ;
22+ const pi = raw [ "profileImage" ] ;
23+ const profileImage =
24+ pi != null && typeof pi === "object"
25+ ? {
26+ alt : ( pi as Record < string , unknown > ) [ "alt" ] ?? "" ,
27+ id : ( pi as Record < string , unknown > ) [ "id" ] ,
28+ }
29+ : pi ;
30+ return {
31+ id : raw [ "id" ] ,
32+ name : raw [ "name" ] ,
33+ profileImage,
34+ username : raw [ "username" ] ,
35+ } ;
36+ }
37+
38+ function trimPostForClientPreload ( raw : Record < string , unknown > ) : Record < string , unknown > {
39+ const images = raw [ "images" ] ;
40+ const trimmedImages = Array . isArray ( images )
41+ ? images . map ( ( im ) => {
42+ const row = im as Record < string , unknown > ;
43+ return { alt : row [ "alt" ] ?? "" , id : row [ "id" ] } ;
44+ } )
45+ : images ;
46+
47+ const movie = raw [ "movie" ] ;
48+ const trimmedMovie =
49+ movie != null && typeof movie === "object"
50+ ? { id : ( movie as Record < string , unknown > ) [ "id" ] }
51+ : movie ;
52+
53+ const sound = raw [ "sound" ] ;
54+ const trimmedSound =
55+ sound != null && typeof sound === "object"
56+ ? { id : ( sound as Record < string , unknown > ) [ "id" ] }
57+ : sound ;
58+
59+ return {
60+ createdAt : raw [ "createdAt" ] ,
61+ id : raw [ "id" ] ,
62+ images : trimmedImages ,
63+ movie : trimmedMovie ,
64+ sound : trimmedSound ,
65+ text : raw [ "text" ] ,
66+ user : trimUserForClientPreload ( raw [ "user" ] ) ,
67+ } ;
68+ }
69+
70+ /** 投稿詳細の LCP 候補(先頭画像 or 動画 GIF)を先読みして、JS 実行前から取得を開始する */
71+ function buildPostDetailLcpPreloadTags ( post : Record < string , unknown > | null ) : string {
72+ if ( post == null ) return "" ;
73+ const parts : string [ ] = [ ] ;
74+ const images = post [ "images" ] ;
75+ if ( Array . isArray ( images ) && images . length > 0 ) {
76+ const first = images [ 0 ] as { id ?: string } | undefined ;
77+ if ( first ?. id != null && first . id !== "" ) {
78+ parts . push (
79+ `<link rel="preload" as="image" href="/images/${ first . id } .jpg" fetchpriority="high">` ,
80+ ) ;
81+ }
82+ }
83+ const movie = post [ "movie" ] as { id ?: string } | null | undefined ;
84+ if ( movie != null && typeof movie === "object" && movie . id != null && movie . id !== "" ) {
85+ parts . push (
86+ `<link rel="preload" as="image" href="/movies/${ movie . id } .gif" fetchpriority="high">` ,
87+ ) ;
88+ }
89+ return parts . join ( "" ) ;
90+ }
91+
1892// index.html テンプレートをキャッシュ
1993let _htmlTemplate : string | null = null ;
2094function getHtmlTemplate ( ) : string {
@@ -139,7 +213,8 @@ async function buildPreloadData(req: Parameters<Parameters<typeof Router>[0]>[0]
139213 // 投稿詳細ページ
140214 const postId = postMatch [ 1 ] ;
141215 const post = await Post . findByPk ( postId ) ;
142- data [ `/api/v1/posts/${ postId } ` ] = post ? post . toJSON ( ) : null ;
216+ data [ `/api/v1/posts/${ postId } ` ] =
217+ post != null ? trimPostForClientPreload ( post . toJSON ( ) as Record < string , unknown > ) : null ;
143218 }
144219 }
145220
@@ -255,8 +330,14 @@ staticRouter.use(async (req, res, next) => {
255330
256331 try {
257332 const preloadData = await buildPreloadData ( req ) ;
333+ const postMatch = req . path . match ( / ^ \/ p o s t s \/ ( [ ^ / ] + ) $ / ) ;
334+ const postKey = postMatch != null ? `/api/v1/posts/${ postMatch [ 1 ] } ` : null ;
335+ const postPayload =
336+ postKey != null && postKey in preloadData ? ( preloadData [ postKey ] as Record < string , unknown > | null ) : null ;
337+ const lcpPreload = postKey != null ? buildPostDetailLcpPreloadTags ( postPayload ) : "" ;
258338 const script = `<script>window.__PRELOAD_DATA__=${ JSON . stringify ( preloadData ) } ;</script>` ;
259- const html = template . replace ( "</head>" , `${ script } </head>` ) ;
339+ let html = lcpPreload !== "" ? template . replace ( "<head>" , `<head>${ lcpPreload } ` ) : template ;
340+ html = html . replace ( "</head>" , `${ script } </head>` ) ;
260341 return res . type ( "text/html" ) . send ( html ) ;
261342 } catch {
262343 return next ( ) ;
0 commit comments