[{"data":1,"prerenderedAt":458},["ShallowReactive",2],{"blog-cls-image-dimension-fix":3,"blog-prev-cls-image-dimension-fix":263,"blog-next-cls-image-dimension-fix":362},{"id":4,"title":5,"author":6,"body":7,"canonical":247,"category":248,"cover":247,"date":249,"description":250,"draft":251,"extension":252,"keywords":247,"meta":253,"navigation":254,"noindex":251,"ogImage":247,"path":255,"seo":256,"seoDescription":247,"seoTitle":247,"stem":257,"tags":258,"updated":247,"__hash__":262},"blog\u002Fblog\u002Fcls-image-dimension-fix.md","给图片和嵌入组件写死尺寸，CLS 一周从零点三降到零点零九","元析智能",{"type":8,"value":9,"toc":240},"minimark",[10,14,19,22,91,102,105,109,112,202,209,212,215,218,221,224,227,230,233,236],[11,12,13],"p",{},"上个月我们给一个开发者工具站做 CLS 整改，从 0.31 降到 0.09，用了一周。动的是 168 个没写尺寸的图片和 6 个嵌入组件。方法不新鲜，难的是把全站扫干净，一个都别漏。",[15,16,18],"h2",{"id":17},"图片给每个-img-补上宽高","图片：给每个 img 补上宽高",[11,20,21],{},"CLS 的根源是浏览器不知道元素最终多大，等内容到了再撑开。宽高属性是成本最低的修法，现代浏览器会拿这两个数自动算宽高比、预留空间。",[23,24,29],"pre",{"className":25,"code":26,"language":27,"meta":28,"style":28},"language-html shiki shiki-themes github-light github-dark","\u003Cimg src=\"dashboard.webp\" width=\"1440\" height=\"810\" alt=\"产品后台截图\" loading=\"lazy\">\n","html","",[30,31,32],"code",{"__ignoreMap":28},[33,34,37,41,45,49,52,56,59,61,64,67,69,72,75,77,80,83,85,88],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sVt8B","\u003C",[33,42,44],{"class":43},"s9eBZ","img",[33,46,48],{"class":47},"sScJk"," src",[33,50,51],{"class":39},"=",[33,53,55],{"class":54},"sZZnC","\"dashboard.webp\"",[33,57,58],{"class":47}," width",[33,60,51],{"class":39},[33,62,63],{"class":54},"\"1440\"",[33,65,66],{"class":47}," height",[33,68,51],{"class":39},[33,70,71],{"class":54},"\"810\"",[33,73,74],{"class":47}," alt",[33,76,51],{"class":39},[33,78,79],{"class":54},"\"产品后台截图\"",[33,81,82],{"class":47}," loading",[33,84,51],{"class":39},[33,86,87],{"class":54},"\"lazy\"",[33,89,90],{"class":39},">\n",[11,92,93,94,97,98,101],{},"有了这两个属性，图片下载前就能按 16:9 占位。响应式图片再配上 ",[30,95,96],{},"sizes"," 和 ",[30,99,100],{},"srcset","，占位比例依然成立。",[11,103,104],{},"查法：跑一遍全站爬虫，把没有 width 和 height 的 img 列出来。我们这个站 312 张图里有 168 张缺尺寸，主要来自博客正文和客户案例页，编辑器上传时没做校验。",[15,106,108],{"id":107},"嵌入组件iframe-也要占位","嵌入组件：iframe 也要占位",[11,110,111],{},"地图、演示视频、客服 iframe 经常从 0 高度起步，加载完撑到 400 多像素。改法是外层容器写死比例，iframe 填满容器。",[23,113,117],{"className":114,"code":115,"language":116,"meta":28,"style":28},"language-css shiki shiki-themes github-light github-dark",".embed { aspect-ratio: 16 \u002F 9; width: 100%; }\n.embed iframe { width: 100%; height: 100%; border: 0; }\n","css",[30,118,119,161],{"__ignoreMap":28},[33,120,121,124,127,131,134,137,140,143,146,149,151,154,158],{"class":35,"line":36},[33,122,123],{"class":47},".embed",[33,125,126],{"class":39}," { ",[33,128,130],{"class":129},"sj4cs","aspect-ratio",[33,132,133],{"class":39},": ",[33,135,136],{"class":129},"16",[33,138,139],{"class":39}," \u002F ",[33,141,142],{"class":129},"9",[33,144,145],{"class":39},"; ",[33,147,148],{"class":129},"width",[33,150,133],{"class":39},[33,152,153],{"class":129},"100",[33,155,157],{"class":156},"szBVR","%",[33,159,160],{"class":39},"; }\n",[33,162,164,166,169,171,173,175,177,179,181,184,186,188,190,192,195,197,200],{"class":35,"line":163},2,[33,165,123],{"class":47},[33,167,168],{"class":43}," iframe",[33,170,126],{"class":39},[33,172,148],{"class":129},[33,174,133],{"class":39},[33,176,153],{"class":129},[33,178,157],{"class":156},[33,180,145],{"class":39},[33,182,183],{"class":129},"height",[33,185,133],{"class":39},[33,187,153],{"class":129},[33,189,157],{"class":156},[33,191,145],{"class":39},[33,193,194],{"class":129},"border",[33,196,133],{"class":39},[33,198,199],{"class":129},"0",[33,201,160],{"class":39},[11,203,204,205,208],{},"这个站的定价页嵌了三个客户评价视频，原来每个都从 0 撑到 320 像素，改完位移归零。顺手把 YouTube 的嵌入换成 ",[30,206,207],{},"youtube-nocookie"," 加懒加载，首屏也轻了一点。",[15,210,211],{"id":211},"骨架屏要预留真实高度",[11,213,214],{},"骨架屏高度和真实内容不一致，等于位移换了个形式。我们的做法是拿一次真实数据截图量出高度，骨架屏写死这个值，误差控制在 8 像素以内。这个站的后台截图区差了 120 像素，改完才真正消掉。",[15,216,217],{"id":217},"广告位和推荐位",[11,219,220],{},"有广告或者合作推荐的站，位子必须提前占好。常见做法是给容器一个 min-height，里面没内容时也占着。这个站的博客文末有一条合作推荐，原来从 0 撑到 180 像素，用户读到最后一行时正文被推走，改完加了 min-height 就好了。",[11,222,223],{},"这类位子的另一个问题是内容有时不来，占位就一直空着。我们一般给个 3 秒超时，超时后容器收起，收起动作放在用户看不见的区域，不影响 CLS。",[15,225,226],{"id":226},"验收方式",[11,228,229],{},"改完别只看 Lighthouse。用 Chrome Performance 面板录一次移动端加载，看 Experience 轨道还有没有位移；再等两周看 GSC 的真实数据。这个站实验室数据第一天就到 0.02，真实数据两周后稳定在 0.09，差距来自长尾页面——旧博客里有 40 多张历史图片还在漏。",[11,231,232],{},"我们给客户定的验收标准是两件事同时满足：实验室三次测试都在 0.1 以下，真实数据两周后 P75 在 0.1 以下。只看前者容易自欺，只看后者周期太长，项目会拖。",[11,234,235],{},"最后提醒一句：公众号文章和富文本编辑器粘进来的图片最容易漏。我们的做法是在编辑器上传环节强制写入宽高，从源头堵住，不然三个月后又会冒出来。",[237,238,239],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":28,"searchDepth":163,"depth":163,"links":241},[242,243,244,245,246],{"id":17,"depth":163,"text":18},{"id":107,"depth":163,"text":108},{"id":211,"depth":163,"text":211},{"id":217,"depth":163,"text":217},{"id":226,"depth":163,"text":226},null,"技术SEO","2025-11-18","一个开发者工具站 CLS 0.31，不是靠玄学优化的：168 个图片补宽高、6 个 iframe 用 aspect-ratio 占位、骨架屏写死真实高度。附查询方法和两段可直接抄的代码。",false,"md",{},true,"\u002Fblog\u002Fcls-image-dimension-fix",{"title":5,"description":250},"blog\u002Fcls-image-dimension-fix",[259,260,261],"CLS","图片尺寸","布局偏移","NtIb0Arzyvtl8vRSevA0gvsTFcRnzCleytBU6Dx3vKk",{"id":264,"title":265,"author":6,"body":266,"canonical":247,"category":248,"cover":247,"date":351,"description":352,"draft":251,"extension":252,"keywords":247,"meta":353,"navigation":254,"noindex":251,"ogImage":247,"path":354,"seo":355,"seoDescription":247,"seoTitle":247,"stem":356,"tags":357,"updated":247,"__hash__":361},"blog\u002Fblog\u002Fspa-rendering-ssr-ssg-prerender.md","单页应用做 SEO，服务端渲染和预渲染怎么选",{"type":8,"value":267,"toc":344},[268,271,274,281,287,293,297,300,303,306,325,328,331,334,341],[11,269,270],{},"能 SSG 就别 SSR，能 SSR 就别预渲染，能预渲染就别硬扛纯客户端渲染。选型只看两件事：内容更新频率、页面数量。",[15,272,273],{"id":273},"三种方案各自适合谁",[11,275,276,280],{},[277,278,279],"strong",{},"SSG（构建时生成 HTML）","：文档站、营销页、博客、产品功能介绍页。内容不是每分钟变，页面几千到几万个都在射程内。成本最低、速度最快。2025 年 4 月一个开发者文档站 4200 页改 Next.js 的 SSG，初始 HTML 直接带全文，上线两个月收录率 92%，LCP 也从 4.1 秒降到 1.3 秒。",[11,282,283,286],{},[277,284,285],{},"SSR（请求时服务端渲染）","：价格页、搜索结果页、带登录态的个性化页面。需要 Node 服务，运维成本高；缓存配不好，TTFB 会比 SSG 慢一截。适合内容必须实时的场景。",[11,288,289,292],{},[277,290,291],{},"预渲染","：给蜘蛛返回提前渲染好的静态快照，真实用户还是走原 SPA。适合改造成本卡死、只求先被收录的团队。代价是快照会过期，要定时刷新，百度对快照的识别也不如 Google 稳定。",[15,294,296],{"id":295},"_2025-年-7-月的救急改造","2025 年 7 月的救急改造",[11,298,299],{},"一个 SaaS 站 600 个页面，前端是纯 React SPA，初始 HTML 只有挂载点。团队排不出重写排期，先用预渲染顶上：三周内收录从 180 涨到 520。",[11,301,302],{},"问题随之而来。价格页的快照一周才刷一次，中间改价后展示的是旧价格，客服接到一堆投诉。后来把价格页单独改成 SSR，其余页面保留预渲染，才稳定下来。这个组合很常见——高频变化的少数页面走 SSR，长尾页面走静态。",[15,304,305],{"id":305},"判断清单",[307,308,309,313,316,319,322],"ul",{},[310,311,312],"li",{},"内容一周才变一次，页面是列表型 → SSG",[310,314,315],{},"内容每次请求都可能不同 → SSR",[310,317,318],{},"已有 Node 服务、团队有人维护 → SSR 成本可接受",[310,320,321],{},"只求收录、不求实时 → 预渲染",[310,323,324],{},"以上都做不到 → 至少把 title、H1、正文前三段、内链塞进初始 HTML，别全靠 JS",[15,326,327],{"id":327},"成本对照",[11,329,330],{},"我们报过价的三个方案，以 800 个页面的 SaaS 站为例：纯预渲染接入大约 2 到 3 人周，主要成本在快照刷新和监控；SSG 改造 6 到 8 人周，要动路由和数据层；SSR 改造 10 人周以上，还得配 Node 服务、缓存和日志。预算差距在五倍左右，所以选型不能只看\"哪个先进\"。",[15,332,333],{"id":333},"改之前先确认到底缺什么",[11,335,336,337,340],{},"用 ",[30,338,339],{},"curl"," 拉一下页面，看初始 HTML 里有没有正文。有的站其实 SSR 做了，只是 H1 和正文用了懒加载；有的站缺的只是内链。整站重构之前先定位缺口，能省下一大笔预算。",[11,342,343],{},"我们接过一个客户，被外包公司报了 40 万的 SSR 改造，实际问题是模板里把正文渲染在 JS 之后，改模板花了两天。",{"title":28,"searchDepth":163,"depth":163,"links":345},[346,347,348,349,350],{"id":273,"depth":163,"text":273},{"id":295,"depth":163,"text":296},{"id":305,"depth":163,"text":305},{"id":327,"depth":163,"text":327},{"id":333,"depth":163,"text":333},"2025-11-17","三种方案解决同一个问题——让蜘蛛拿到有正文的 HTML，但成本和适用场景差很远。我们用三个客户站的改造结果，给出按站点类型排序的选型顺序和判断清单。",{},"\u002Fblog\u002Fspa-rendering-ssr-ssg-prerender",{"title":265,"description":352},"blog\u002Fspa-rendering-ssr-ssg-prerender",[358,359,360],"渲染","SSR","SPA","1IQG_zGcoYbbtQtpBcIU-WkwxkRCt5rouU8BYQcjKo0",{"id":363,"title":364,"author":6,"body":365,"canonical":247,"category":446,"cover":247,"date":447,"description":448,"draft":251,"extension":252,"keywords":247,"meta":449,"navigation":254,"noindex":251,"ogImage":247,"path":450,"seo":451,"seoDescription":247,"seoTitle":247,"stem":452,"tags":453,"updated":247,"__hash__":457},"blog\u002Fblog\u002Fkeyword-library-four-tables.md","把两万个关键词压成四张表，我们的词库分层法",{"type":8,"value":366,"toc":437},[367,370,373,376,379,382,385,388,392,395,398,401,404,407,410,413,416,419,422,425,428,431,434],[11,368,369],{},"词库建得越大，越没人用。我们的做法是反过来的：两万一千个词进来，最后只留四张表，加起来不到 1200 个词。四张表各有用途，每周五更新一次，谁都能打开看懂。",[15,371,372],{"id":372},"四张表分别放什么",[11,374,375],{},"第一张是核心词，10 到 20 个，决定首页、导航和产品页的定位。第二张是页面词，每个词绑定一个确定的 URL。第三张是长尾池，不绑页面，按主题挂着。第四张是排除词，写明为什么不写。",[11,377,378],{},"四张表之外，别的词都进回收站。需要时再翻，不占位置。",[15,380,381],{"id":381},"核心词这张表最重要",[11,383,384],{},"核心词是客户会用来描述你这个品类的词，通常不超过 20 个。它决定首页标题、导航结构，也决定你未来两年跟谁竞争。",[11,386,387],{},"这张表我们半年才动一次。动一次要重新看竞品和客户说法，不是加一个词那么简单。",[15,389,391],{"id":390},"页面词要写到-url-一级","页面词要写到 URL 一级",[11,393,394],{},"第二张表每行必须有一个 URL，没有 URL 的词打回去。这张表是给排期用的，写稿的人拿到行就能开工。",[11,396,397],{},"我们 2025 年 10 月统计过，这张表里 68% 的词已经有对应页面，剩下 32% 就是下个季度的选题来源。",[15,399,400],{"id":400},"长尾池只记三个字段",[11,402,403],{},"词、意图、归属主题。不记搜索量，不记难度分。长尾池会持续变大，字段一多就没人维护。",[11,405,406],{},"每周五我们从搜索后台导一次新查询，人工过一遍，能用的扔进池子，不能用的进排除词表。",[15,408,409],{"id":409},"排除词表是被低估的一张",[11,411,412],{},"排除词表写清楚“免费、破解、下载、招聘、外包”这一类，也写下“这个词跟我们的产品无关”“这个词搜的人不会买”。",[11,414,415],{},"新同事接手时不看这张表，会把去年删掉的词重新挖回来。我们遇到过两回。",[15,417,418],{"id":418},"我们走过的弯路",[11,420,421],{},"2025 年上半年，我们把词库按 20 个行业分成 20 张表，做得很规整。结果三个月后没人打开，因为找不到该看哪张。",[11,423,424],{},"下半年改成四张表，第一周就有人主动用了。分类不是越细越好，是让人一眼知道去哪张表里找东西。",[15,426,427],{"id":427},"四张表之间怎么流动",[11,429,430],{},"新词默认进长尾池，被证明有承接价值才升进页面词表，被证明没人搜才降进排除词表。每个月做一次升降，只动 20 到 30 个。",[11,432,433],{},"这个流动过程本身就是选题会。词往上走，说明有活儿要干；词往下走，说明以前判断错了。",[11,435,436],{},"词库不是资料，是排期表的上游。上游乱，下游一定乱。",{"title":28,"searchDepth":163,"depth":163,"links":438},[439,440,441,442,443,444,445],{"id":372,"depth":163,"text":372},{"id":381,"depth":163,"text":381},{"id":390,"depth":163,"text":391},{"id":400,"depth":163,"text":400},{"id":409,"depth":163,"text":409},{"id":418,"depth":163,"text":418},{"id":427,"depth":163,"text":427},"SEO","2025-11-19","词库的价值在删不在存。两万一千个词经过三层过滤，最后只留四张表：核心词、页面词、长尾池、排除词。每张表有明确用途和负责人，每周五更新一次。",{},"\u002Fblog\u002Fkeyword-library-four-tables",{"title":364,"description":448},"blog\u002Fkeyword-library-four-tables",[454,455,456],"词库搭建","关键词研究","内容排期","0X3kYHpm_KvMf_jzr50-tPVqtF9vZm2Trw36Yh0kMNk",1794088110748]