[{"data":1,"prerenderedAt":598},["ShallowReactive",2],{"post-\u002Fposts\u002Fno-loading-feel":3,"post-nav-\u002Fposts\u002Fno-loading-feel":566,"post-related-\u002Fposts\u002Fno-loading-feel":585},{"id":4,"title":5,"body":6,"category":551,"cover":552,"date":553,"description":554,"draft":555,"extension":556,"featured":555,"meta":557,"navigation":303,"path":558,"pinned":555,"seo":559,"stem":560,"tags":561,"updated":552,"__hash__":565},"posts\u002Fposts\u002Fno-loading-feel.md","无加载不是速度，是连续感",{"type":7,"value":8,"toc":541},"minimark",[9,13,16,19,22,29,32,37,43,46,59,63,66,69,76,125,128,134,162,166,169,172,175,182,240,383,386,392,396,399,402,425,428,435,441,444,447,455,458,464,467,470,476,482,488,491,494,501,504,519,522,525,534,537],[10,11,12],"p",{},"先做一个思想实验。",[10,14,15],{},"方案 A：点击链接后，页面白屏 300ms，然后新页面完整出现。",[10,17,18],{},"方案 B：点击链接后，立刻显示骨架屏，800ms 后内容填进来。",[10,20,21],{},"哪个更快？A。",[10,23,24,25],{},"哪个感觉更快？",[26,27,28],"strong",{},"A。",[10,30,31],{},"骨架屏不是为了让页面\"看起来在加载\"，它是在承认\"我在加载\"。而这个承认本身，就是一种延迟信号。",[33,34,36],"h2",{"id":35},"真正的无加载是什么","真正的「无加载」是什么",[10,38,39,40],{},"无加载的意思是：",[26,41,42],{},"用户从未看到\"等待\"这个状态。",[10,44,45],{},"实现它需要三个条件同时成立：",[47,48,49,53,56],"ol",{},[50,51,52],"li",{},"页面在点击之前就已经准备好了",[50,54,55],{},"切换过程不出现空白帧",[50,57,58],{},"没有任何 loading 指示器",[33,60,62],{"id":61},"条件一提前准备","条件一：提前准备",[10,64,65],{},"SSG（静态生成）把这件事做到了极致：所有页面在构建时就变成了 HTML 文件。",[10,67,68],{},"点击链接时，浏览器请求的是一个已经存在的静态文件——不是\"等服务器查询数据库再渲染\"。",[10,70,71,72,75],{},"但即使如此，仍然有一次网络往返。所以还需要",[26,73,74],{},"预取","：",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-html shiki shiki-themes github-light github-dark","\u003C!-- Nuxt 的 NuxtLink 默认开启 prefetch -->\n\u003CNuxtLink to=\"\u002Farchive\">归档\u003C\u002FNuxtLink>\n","html","",[84,85,86,95],"code",{"__ignoreMap":82},[87,88,91],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"sJ8bj","\u003C!-- Nuxt 的 NuxtLink 默认开启 prefetch -->\n",[87,96,98,102,106,110,113,117,120,122],{"class":89,"line":97},2,[87,99,101],{"class":100},"sVt8B","\u003C",[87,103,105],{"class":104},"s7hpK","NuxtLink",[87,107,109],{"class":108},"sScJk"," to",[87,111,112],{"class":100},"=",[87,114,116],{"class":115},"sZZnC","\"\u002Farchive\"",[87,118,119],{"class":100},">归档\u003C\u002F",[87,121,105],{"class":104},[87,123,124],{"class":100},">\n",[10,126,127],{},"Nuxt 会在链接进入视口时预取目标页面的 payload。用户真正点击时，数据已经在内存里了。",[10,129,130,133],{},[26,131,132],{},"关键细节","：外链和友链要显式关闭预取，否则会预取一堆无关域名。",[77,135,137],{"className":79,"code":136,"language":81,"meta":82,"style":82},"\u003CNuxtLink to=\"\u002Flinks\" no-prefetch>友链\u003C\u002FNuxtLink>\n",[84,138,139],{"__ignoreMap":82},[87,140,141,143,145,147,149,152,155,158,160],{"class":89,"line":90},[87,142,101],{"class":100},[87,144,105],{"class":104},[87,146,109],{"class":108},[87,148,112],{"class":100},[87,150,151],{"class":115},"\"\u002Flinks\"",[87,153,154],{"class":108}," no-prefetch",[87,156,157],{"class":100},">友链\u003C\u002F",[87,159,105],{"class":104},[87,161,124],{"class":100},[33,163,165],{"id":164},"条件二不出现空白帧","条件二：不出现空白帧",[10,167,168],{},"即使数据准备好了，页面切换仍然会有一次 DOM 重建。",[10,170,171],{},"如果没有过渡，你会看到：旧页面消失 → 空白 → 新页面出现。",[10,173,174],{},"那\"空白\"可能只有 16ms（一帧），但眼睛能看见。",[10,176,177,178,181],{},"解法是 ",[84,179,180],{},"out-in"," 模式的页面过渡：",[77,183,187],{"className":184,"code":185,"language":186,"meta":82,"style":82},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F nuxt.config.ts\napp: {\n  pageTransition: { name: 'page', mode: 'out-in' },\n}\n","ts",[84,188,189,194,202,234],{"__ignoreMap":82},[87,190,191],{"class":89,"line":90},[87,192,193],{"class":93},"\u002F\u002F nuxt.config.ts\n",[87,195,196,199],{"class":89,"line":97},[87,197,198],{"class":108},"app",[87,200,201],{"class":100},": {\n",[87,203,205,208,211,214,217,220,223,226,228,231],{"class":89,"line":204},3,[87,206,207],{"class":108},"  pageTransition",[87,209,210],{"class":100},": { ",[87,212,213],{"class":108},"name",[87,215,216],{"class":100},": ",[87,218,219],{"class":115},"'page'",[87,221,222],{"class":100},", ",[87,224,225],{"class":108},"mode",[87,227,216],{"class":100},[87,229,230],{"class":115},"'out-in'",[87,232,233],{"class":100}," },\n",[87,235,237],{"class":89,"line":236},4,[87,238,239],{"class":100},"}\n",[77,241,245],{"className":242,"code":243,"language":244,"meta":82,"style":82},"language-css shiki shiki-themes github-light github-dark",".page-enter-active,\n.page-leave-active {\n  transition: opacity 200ms ease-out, transform 200ms ease-out;\n}\n\n.page-enter-from {\n  opacity: 0;\n  transform: translateY(4px);\n}\n\n.page-leave-to {\n  opacity: 0;\n}\n","css",[84,246,247,255,263,294,298,305,313,326,349,354,359,367,378],{"__ignoreMap":82},[87,248,249,252],{"class":89,"line":90},[87,250,251],{"class":108},".page-enter-active",[87,253,254],{"class":100},",\n",[87,256,257,260],{"class":89,"line":97},[87,258,259],{"class":108},".page-leave-active",[87,261,262],{"class":100}," {\n",[87,264,265,269,272,275,279,282,285,287,289,291],{"class":89,"line":204},[87,266,268],{"class":267},"sj4cs","  transition",[87,270,271],{"class":100},": opacity ",[87,273,274],{"class":267},"200",[87,276,278],{"class":277},"szBVR","ms",[87,280,281],{"class":267}," ease-out",[87,283,284],{"class":100},", transform ",[87,286,274],{"class":267},[87,288,278],{"class":277},[87,290,281],{"class":267},[87,292,293],{"class":100},";\n",[87,295,296],{"class":89,"line":236},[87,297,239],{"class":100},[87,299,301],{"class":89,"line":300},5,[87,302,304],{"emptyLinePlaceholder":303},true,"\n",[87,306,308,311],{"class":89,"line":307},6,[87,309,310],{"class":108},".page-enter-from",[87,312,262],{"class":100},[87,314,316,319,321,324],{"class":89,"line":315},7,[87,317,318],{"class":267},"  opacity",[87,320,216],{"class":100},[87,322,323],{"class":267},"0",[87,325,293],{"class":100},[87,327,329,332,334,337,340,343,346],{"class":89,"line":328},8,[87,330,331],{"class":267},"  transform",[87,333,216],{"class":100},[87,335,336],{"class":267},"translateY",[87,338,339],{"class":100},"(",[87,341,342],{"class":267},"4",[87,344,345],{"class":277},"px",[87,347,348],{"class":100},");\n",[87,350,352],{"class":89,"line":351},9,[87,353,239],{"class":100},[87,355,357],{"class":89,"line":356},10,[87,358,304],{"emptyLinePlaceholder":303},[87,360,362,365],{"class":89,"line":361},11,[87,363,364],{"class":108},".page-leave-to",[87,366,262],{"class":100},[87,368,370,372,374,376],{"class":89,"line":369},12,[87,371,318],{"class":267},[87,373,216],{"class":100},[87,375,323],{"class":267},[87,377,293],{"class":100},[87,379,381],{"class":89,"line":380},13,[87,382,239],{"class":100},[10,384,385],{},"200ms 的淡入淡出。旧页面淡出的同时新页面淡入，中间没有空白帧。",[10,387,388,391],{},[26,389,390],{},"为什么是 200ms？"," 低于 150ms 感知不到，高于 300ms 开始觉得慢。200ms 是\"有过渡但不等待\"的窗口。",[33,393,395],{"id":394},"条件三删掉-loading-指示器","条件三：删掉 loading 指示器",[10,397,398],{},"这一条最反直觉。",[10,400,401],{},"大部分项目会加一个顶部进度条（NuxtLoadingIndicator）：",[77,403,407],{"className":404,"code":405,"language":406,"meta":82,"style":82},"language-vue shiki shiki-themes github-light github-dark","\u003C!-- 删掉这个 -->\n\u003CNuxtLoadingIndicator \u002F>\n","vue",[84,408,409,414],{"__ignoreMap":82},[87,410,411],{"class":89,"line":90},[87,412,413],{"class":93},"\u003C!-- 删掉这个 -->\n",[87,415,416,418,422],{"class":89,"line":97},[87,417,101],{"class":100},[87,419,421],{"class":420},"s9eBZ","NuxtLoadingIndicator",[87,423,424],{"class":100}," \u002F>\n",[10,426,427],{},"进度条的存在本身就在告诉用户：\"可能要等一下\"。",[10,429,430,431,434],{},"当你的页面跳转真的在 200ms 内完成时，进度条会",[26,432,433],{},"闪一下","——比没有更糟。",[10,436,437,438,440],{},"所以我把 ",[84,439,421],{}," 完全删掉了，靠 view transition 自然过渡。",[33,442,443],{"id":443},"一个真实的对比",[10,445,446],{},"改之前（有骨架屏 + 进度条）：",[77,448,453],{"className":449,"code":451,"language":452},[450],"language-text","点击 → 进度条出现 → 骨架屏 → 内容填入 → 进度条消失\n总时长：约 600ms，用户全程在\"等待\"\n","text",[84,454,451],{"__ignoreMap":82},[10,456,457],{},"改之后（SSG + 预取 + 过渡）：",[77,459,462],{"className":460,"code":461,"language":452},[450],"点击 → 淡出 → 淡入\n总时长：约 200ms，用户感知为\"瞬间\"\n",[84,463,461],{"__ignoreMap":82},[33,465,466],{"id":466},"什么情况下做不到",[10,468,469],{},"诚实地说，这套方案有三个前提：",[10,471,472,475],{},[26,473,474],{},"一、内容必须是静态的。"," 如果页面需要实时数据（股票、聊天），SSG 帮不上忙。",[10,477,478,481],{},[26,479,480],{},"二、页面数量要可控。"," 全量预渲染 10 万个页面，构建要几小时。",[10,483,484,487],{},[26,485,486],{},"三、首屏没法这样优化。"," 用户第一次访问仍然要下载 HTML + CSS + JS。这是物理限制。",[10,489,490],{},"对一个个人博客来说，这三个前提全都成立——所以它能做到真正的无加载。",[33,492,493],{"id":493},"首屏怎么办",[10,495,496,497,500],{},"首屏的唯一优化方向是",[26,498,499],{},"减小体积","。",[10,502,503],{},"我的做法：",[505,506,507,510,513,516],"ul",{},[50,508,509],{},"没有第三方脚本（统计默认关闭）",[50,511,512],{},"字体分片，实际加载约 180KB",[50,514,515],{},"搜索索引懒加载（首次搜索才请求）",[50,517,518],{},"零运行时状态库（没有 Pinia）",[10,520,521],{},"结果：首屏 JS 约 41KB（gzip 后）。",[33,523,524],{"id":524},"一句话",[526,527,528],"blockquote",{},[10,529,530,531,500],{},"无加载不是把加载藏起来，是",[26,532,533],{},"让加载根本不需要发生",[10,535,536],{},"这两者的区别在于：前者是障眼法，后者是架构。",[538,539,540],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#57606A;--shiki-dark:#9AA4B0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}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}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":82,"searchDepth":97,"depth":97,"links":542},[543,544,545,546,547,548,549,550],{"id":35,"depth":97,"text":36},{"id":61,"depth":97,"text":62},{"id":164,"depth":97,"text":165},{"id":394,"depth":97,"text":395},{"id":443,"depth":97,"text":443},{"id":466,"depth":97,"text":466},{"id":493,"depth":97,"text":493},{"id":524,"depth":97,"text":524},"前端",null,"2025-05-15","一个 300ms 的白屏比一个 800ms 的骨架屏更让人焦虑。这篇讲怎么用 SSG、预取和 view transition 把页面跳转做成「一瞬间的事」。",false,"md",{},"\u002Fposts\u002Fno-loading-feel",{"title":5,"description":554},"posts\u002Fno-loading-feel",[562,563,564],"性能","体验","Nuxt","AZHYZXvoMeOlBm-ojJcpeaJ9atLEwrH6cIrYsZyEmWY",{"prev":567,"next":577},{"path":568,"title":569,"description":570,"date":571,"updated":552,"tags":572,"category":551,"cover":576,"draft":555,"pinned":555,"featured":303},"\u002Fposts\u002Fview-transitions","View Transitions 不是动画库","用它做页面切换、做主题扩散、做列表到详情的共享元素过渡。它真正的价值不是\"让页面动起来\"，而是让你不必手动管理进入和离开两套状态。","2025-09-18",[573,574,575],"动画","浏览器","CSS","\u002Fimages\u002Fcovers\u002Fview-transition.png",{"path":578,"title":579,"description":580,"date":581,"updated":552,"tags":582,"category":551,"cover":552,"draft":555,"pinned":555,"featured":555},"\u002Fposts\u002Fcss-variables-as-api","我把 CSS 变量当成了 API","设计 token 写进配置文件，构建期编译成 CSS 变量，组件只读变量。这套做法让「改一处，全站生效」从口号变成了构建期保证。","2025-04-20",[575,583,584],"设计系统","构建",[586,594,596],{"path":587,"title":588,"description":589,"date":590,"updated":552,"tags":591,"category":551,"cover":552,"draft":555,"pinned":555,"featured":555},"\u002Fposts\u002Fself-hosted-fonts","自托管字体的一年","从 Google Fonts 换成自托管，从自托管换成子集化分片。聊聊为什么一个中文字体能从 25MB 变成实际加载 180KB。","2025-08-08",[592,562,593],"字体","中文",{"path":568,"title":569,"description":570,"date":571,"updated":552,"tags":595,"category":551,"cover":576,"draft":555,"pinned":555,"featured":303},[573,574,575],{"path":578,"title":579,"description":580,"date":581,"updated":552,"tags":597,"category":551,"cover":552,"draft":555,"pinned":555,"featured":555},[575,583,584],1794113902326]