[{"data":1,"prerenderedAt":1624},["ShallowReactive",2],{"en-post-\u002Fen\u002Fnuxt-usefetch-await-blocks-navigation":3},{"id":4,"title":5,"body":6,"description":1611,"extension":1612,"meta":1613,"navigation":825,"path":1619,"seo":1620,"sitemap":1621,"stem":1622,"__hash__":1623},"blogEn\u002Fen\u002Fnuxt-usefetch-await-blocks-navigation.md","Why await useFetch Freezes Page Navigation in Nuxt: Removing a 2.4s Wait with useLazyFetch",{"type":7,"value":8,"toc":1596},"minimark",[9,14,23,26,105,112,115,157,160,164,182,185,276,322,335,340,398,410,434,436,440,450,491,689,696,701,703,707,919,959,971,980,982,986,997,1221,1228,1232,1239,1325,1329,1361,1420,1422,1426,1476,1479,1533,1545,1562,1566,1592],[10,11,13],"h2",{"id":12},"click-and-nothing-happens-for-24-seconds","Click, and nothing happens for 2.4 seconds",[15,16,17,18,22],"p",{},"I was building a tourism info service with Nuxt. Clicking a spot in the list opens its detail page, but ",[19,20,21],"strong",{},"for 2.4 seconds after the click, the screen didn't change."," With no loading indicator and the list still showing, it felt like the click hadn't registered.",[15,24,25],{},"The detail page code looked like this:",[27,28,33],"pre",{"className":29,"code":30,"language":31,"meta":32,"style":32},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F pages\u002Fspots\u002F[id].vue\nconst { data: spot } = await useFetch(() => `\u002Fapi\u002Fspots\u002F${id.value}`)\n","js","",[34,35,36,45],"code",{"__ignoreMap":32},[37,38,41],"span",{"class":39,"line":40},"line",1,[37,42,44],{"class":43},"sJ8bj","\u002F\u002F pages\u002Fspots\u002F[id].vue\n",[37,46,48,52,56,60,63,67,70,73,76,80,83,86,90,93,96,99,102],{"class":39,"line":47},2,[37,49,51],{"class":50},"szBVR","const",[37,53,55],{"class":54},"sVt8B"," { ",[37,57,59],{"class":58},"s4XuR","data",[37,61,62],{"class":54},": ",[37,64,66],{"class":65},"sj4cs","spot",[37,68,69],{"class":54}," } ",[37,71,72],{"class":50},"=",[37,74,75],{"class":50}," await",[37,77,79],{"class":78},"sScJk"," useFetch",[37,81,82],{"class":54},"(() ",[37,84,85],{"class":50},"=>",[37,87,89],{"class":88},"sZZnC"," `\u002Fapi\u002Fspots\u002F${",[37,91,92],{"class":54},"id",[37,94,95],{"class":88},".",[37,97,98],{"class":54},"value",[37,100,101],{"class":88},"}`",[37,103,104],{"class":54},")\n",[15,106,107,108,111],{},"And the detail API fetched data from the Korea Tourism Organization API, then ",[19,109,110],{},"generated a summary with the Claude API",", returning everything at once. That summary call took 1 to 2 seconds.",[15,113,114],{},"Two causes stacked up:",[116,117,118,131],"table",{},[119,120,121],"thead",{},[122,123,124,128],"tr",{},[125,126,127],"th",{},"Cause",[125,129,130],{},"Effect",[132,133,134,143],"tbody",{},[122,135,136,140],{},[137,138,139],"td",{},"The detail API waited for a slow AI summary before responding",[137,141,142],{},"The detail response itself was 1 to 2 seconds late",[122,144,145,151],{},[137,146,147,148],{},"The page used ",[34,149,150],{},"await useFetch",[137,152,153,156],{},[19,154,155],{},"Navigation stopped"," until that response arrived",[158,159],"hr",{},[10,161,163],{"id":162},"await-usefetch-blocks-navigation","await useFetch blocks navigation",[165,166,167],"blockquote",{},[15,168,169,170,173,174,177,178,181],{},"When you ",[34,171,172],{},"await"," ",[34,175,176],{},"useFetch",", Nuxt ",[19,179,180],{},"doesn't move to the next page until the data arrives."," Meanwhile, the user keeps looking at the previous page.",[15,183,184],{},"That's easier to feel than to read, so I built an API that takes 2 seconds and measured it: the time from clicking a link on a list page until the next page's heading appears.",[27,186,188],{"className":29,"code":187,"language":31,"meta":32,"style":32},"\u002F\u002F a test API that responds after 2 seconds\nexport default defineEventHandler(async () => {\n  await new Promise((r) => setTimeout(r, 2000))\n  return { ok: true }\n})\n",[34,189,190,195,220,255,270],{"__ignoreMap":32},[37,191,192],{"class":39,"line":40},[37,193,194],{"class":43},"\u002F\u002F a test API that responds after 2 seconds\n",[37,196,197,200,203,206,209,212,215,217],{"class":39,"line":47},[37,198,199],{"class":50},"export",[37,201,202],{"class":50}," default",[37,204,205],{"class":78}," defineEventHandler",[37,207,208],{"class":54},"(",[37,210,211],{"class":50},"async",[37,213,214],{"class":54}," () ",[37,216,85],{"class":50},[37,218,219],{"class":54}," {\n",[37,221,223,226,229,232,235,238,241,243,246,249,252],{"class":39,"line":222},3,[37,224,225],{"class":50},"  await",[37,227,228],{"class":50}," new",[37,230,231],{"class":65}," Promise",[37,233,234],{"class":54},"((",[37,236,237],{"class":58},"r",[37,239,240],{"class":54},") ",[37,242,85],{"class":50},[37,244,245],{"class":78}," setTimeout",[37,247,248],{"class":54},"(r, ",[37,250,251],{"class":65},"2000",[37,253,254],{"class":54},"))\n",[37,256,258,261,264,267],{"class":39,"line":257},4,[37,259,260],{"class":50},"  return",[37,262,263],{"class":54}," { ok: ",[37,265,266],{"class":65},"true",[37,268,269],{"class":54}," }\n",[37,271,273],{"class":39,"line":272},5,[37,274,275],{"class":54},"})\n",[116,277,278,291],{},[119,279,280],{},[122,281,282,285,288],{},[125,283,284],{},"Page code",[125,286,287],{},"Until the next page shows",[125,289,290],{},"Until the data arrives",[132,292,293,307],{},[122,294,295,300,305],{},[137,296,297],{},[34,298,299],{},"await useFetch(...)",[137,301,302],{},[19,303,304],{},"about 2,400ms",[137,306,304],{},[122,308,309,314,319],{},[137,310,311],{},[34,312,313],{},"useLazyFetch(...)",[137,315,316],{},[19,317,318],{},"about 100ms",[137,320,321],{},"about 2,100ms",[15,323,324,325,173,328,330,331,334],{},"The data takes about 2 seconds either way. What differs is ",[19,326,327],{},"when the screen changes.",[34,329,150],{}," stays on the previous page until the data arrives; ",[34,332,333],{},"useLazyFetch"," moves to the next page right away and waits for the data there. (Nuxt 3.21 dev server, Chromium, measured twice each.)",[336,337,339],"h3",{"id":338},"uselazyfetch-is-usefetch-with-lazy-on","useLazyFetch is useFetch with lazy on",[27,341,343],{"className":29,"code":342,"language":31,"meta":32,"style":32},"\u002F\u002F these are the same\nconst { data } = useLazyFetch('\u002Fapi\u002Fspots\u002F1')\nconst { data } = useFetch('\u002Fapi\u002Fspots\u002F1', { lazy: true })\n",[34,344,345,350,372],{"__ignoreMap":32},[37,346,347],{"class":39,"line":40},[37,348,349],{"class":43},"\u002F\u002F these are the same\n",[37,351,352,354,356,358,360,362,365,367,370],{"class":39,"line":47},[37,353,51],{"class":50},[37,355,55],{"class":54},[37,357,59],{"class":65},[37,359,69],{"class":54},[37,361,72],{"class":50},[37,363,364],{"class":78}," useLazyFetch",[37,366,208],{"class":54},[37,368,369],{"class":88},"'\u002Fapi\u002Fspots\u002F1'",[37,371,104],{"class":54},[37,373,374,376,378,380,382,384,386,388,390,393,395],{"class":39,"line":222},[37,375,51],{"class":50},[37,377,55],{"class":54},[37,379,59],{"class":65},[37,381,69],{"class":54},[37,383,72],{"class":50},[37,385,79],{"class":78},[37,387,208],{"class":54},[37,389,369],{"class":88},[37,391,392],{"class":54},", { lazy: ",[37,394,266],{"class":65},[37,396,397],{"class":54}," })\n",[15,399,400,401,404,405],{},"With ",[34,402,403],{},"lazy",", Nuxt shows the page without waiting for data. In exchange, the page has to handle ",[19,406,407,409],{},[34,408,59],{}," being empty at first.",[411,412,413],"warning",{},[165,414,415],{},[15,416,417,420,421,423,424,427,428,430,431],{},[19,418,419],{},"On a first visit, both wait."," When someone types the URL or refreshes, the server fetches the data while building the HTML (server-side rendering). Then ",[34,422,333],{}," also waits for the data before sending HTML. Requesting the same test pages with ",[34,425,426],{},"curl"," took about 2 seconds for both, and the HTML had the data filled in. What ",[34,429,403],{}," removes is the wait ",[19,432,433],{},"when navigating within the app.",[158,435],{},[10,437,439],{"id":438},"fix-1-move-the-slow-ai-summary-to-its-own-api","Fix 1: Move the slow AI summary to its own API",[15,441,442,443,445,446,449],{},"Before switching to ",[34,444,333],{},", there was something to do first: ",[19,447,448],{},"make the response itself fast."," I removed the AI summary call from the detail API and added a separate API that returns only the summary. (The code below is trimmed to what matters here.)",[27,451,455],{"className":452,"code":453,"language":454,"meta":32,"style":32},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F server\u002Fapi\u002Fspots\u002F[id].get.ts — detail: no more AI summary call\nconst detail = toSpotDetail(item, intro, images)\n\u002F\u002F No AI summary here. The Claude call takes 1-2s and would delay the whole detail response.\nreturn detail\n","ts",[34,456,457,462,478,483],{"__ignoreMap":32},[37,458,459],{"class":39,"line":40},[37,460,461],{"class":43},"\u002F\u002F server\u002Fapi\u002Fspots\u002F[id].get.ts — detail: no more AI summary call\n",[37,463,464,466,469,472,475],{"class":39,"line":47},[37,465,51],{"class":50},[37,467,468],{"class":65}," detail",[37,470,471],{"class":50}," =",[37,473,474],{"class":78}," toSpotDetail",[37,476,477],{"class":54},"(item, intro, images)\n",[37,479,480],{"class":39,"line":222},[37,481,482],{"class":43},"\u002F\u002F No AI summary here. The Claude call takes 1-2s and would delay the whole detail response.\n",[37,484,485,488],{"class":39,"line":257},[37,486,487],{"class":50},"return",[37,489,490],{"class":54}," detail\n",[27,492,494],{"className":452,"code":493,"language":454,"meta":32,"style":32},"\u002F\u002F server\u002Fapi\u002Fspots\u002F[id]\u002Fsummary.get.ts — summary only\nexport default defineEventHandler(async (event) => {\n  const contentId = getRouterParam(event, 'id')\n  try {\n    const { name, overview, summary } = await getSpot(contentId) \u002F\u002F shares the detail cache\n    if (!overview) return { summary: null }\n    const aiSummary = await summarizeWithAI(contentId, name, overview)\n    return { summary: aiSummary ?? summary ?? null }\n  } catch {\n    return { summary: null } \u002F\u002F the page works without a summary, so don't throw\n  }\n})\n",[34,495,496,501,525,546,553,589,613,631,653,664,678,684],{"__ignoreMap":32},[37,497,498],{"class":39,"line":40},[37,499,500],{"class":43},"\u002F\u002F server\u002Fapi\u002Fspots\u002F[id]\u002Fsummary.get.ts — summary only\n",[37,502,503,505,507,509,511,513,516,519,521,523],{"class":39,"line":47},[37,504,199],{"class":50},[37,506,202],{"class":50},[37,508,205],{"class":78},[37,510,208],{"class":54},[37,512,211],{"class":50},[37,514,515],{"class":54}," (",[37,517,518],{"class":58},"event",[37,520,240],{"class":54},[37,522,85],{"class":50},[37,524,219],{"class":54},[37,526,527,530,533,535,538,541,544],{"class":39,"line":222},[37,528,529],{"class":50},"  const",[37,531,532],{"class":65}," contentId",[37,534,471],{"class":50},[37,536,537],{"class":78}," getRouterParam",[37,539,540],{"class":54},"(event, ",[37,542,543],{"class":88},"'id'",[37,545,104],{"class":54},[37,547,548,551],{"class":39,"line":257},[37,549,550],{"class":50},"  try",[37,552,219],{"class":54},[37,554,555,558,560,563,566,569,571,574,576,578,580,583,586],{"class":39,"line":272},[37,556,557],{"class":50},"    const",[37,559,55],{"class":54},[37,561,562],{"class":65},"name",[37,564,565],{"class":54},", ",[37,567,568],{"class":65},"overview",[37,570,565],{"class":54},[37,572,573],{"class":65},"summary",[37,575,69],{"class":54},[37,577,72],{"class":50},[37,579,75],{"class":50},[37,581,582],{"class":78}," getSpot",[37,584,585],{"class":54},"(contentId) ",[37,587,588],{"class":43},"\u002F\u002F shares the detail cache\n",[37,590,592,595,597,600,603,605,608,611],{"class":39,"line":591},6,[37,593,594],{"class":50},"    if",[37,596,515],{"class":54},[37,598,599],{"class":50},"!",[37,601,602],{"class":54},"overview) ",[37,604,487],{"class":50},[37,606,607],{"class":54}," { summary: ",[37,609,610],{"class":65},"null",[37,612,269],{"class":54},[37,614,616,618,621,623,625,628],{"class":39,"line":615},7,[37,617,557],{"class":50},[37,619,620],{"class":65}," aiSummary",[37,622,471],{"class":50},[37,624,75],{"class":50},[37,626,627],{"class":78}," summarizeWithAI",[37,629,630],{"class":54},"(contentId, name, overview)\n",[37,632,634,637,640,643,646,648,651],{"class":39,"line":633},8,[37,635,636],{"class":50},"    return",[37,638,639],{"class":54}," { summary: aiSummary ",[37,641,642],{"class":50},"??",[37,644,645],{"class":54}," summary ",[37,647,642],{"class":50},[37,649,650],{"class":65}," null",[37,652,269],{"class":54},[37,654,656,659,662],{"class":39,"line":655},9,[37,657,658],{"class":54},"  } ",[37,660,661],{"class":50},"catch",[37,663,219],{"class":54},[37,665,667,669,671,673,675],{"class":39,"line":666},10,[37,668,636],{"class":50},[37,670,607],{"class":54},[37,672,610],{"class":65},[37,674,69],{"class":54},[37,676,677],{"class":43},"\u002F\u002F the page works without a summary, so don't throw\n",[37,679,681],{"class":39,"line":680},11,[37,682,683],{"class":54},"  }\n",[37,685,687],{"class":39,"line":686},12,[37,688,275],{"class":54},[15,690,691,692,695],{},"That alone brought the detail API response down to ",[19,693,694],{},"0.1 to 0.4 seconds."," The summary API reads the source data with the same cache key as the detail API, so it doesn't call the tourism API again.",[165,697,698],{},[15,699,700],{},"If slow work is \"extra information the page can live without,\" don't tie it to the main response. Fetch it separately.",[158,702],{},[10,704,706],{"id":705},"fix-2-uselazyfetch-for-the-page-server-false-for-the-summary","Fix 2: useLazyFetch for the page, server: false for the summary",[27,708,710],{"className":29,"code":709,"language":31,"meta":32,"style":32},"\u002F\u002F pages\u002Fspots\u002F[id].vue\n\u002F\u002F detail: show the page without waiting\nconst { data: spot, error, status } = useLazyFetch(() => `\u002Fapi\u002Fspots\u002F${id.value}`, {\n  key: () => `spot-${id.value}`,\n})\nconst pending = computed(() => status.value === 'pending')\n\n\u002F\u002F summary: fetch only in the browser\nconst { data: aiSummary, status: summaryStatus } = useLazyFetch(\n  () => `\u002Fapi\u002Fspots\u002F${id.value}\u002Fsummary`,\n  { key: () => `spot-summary-${id.value}`, server: false },\n)\n",[34,711,712,716,721,766,790,794,821,827,832,863,883,915],{"__ignoreMap":32},[37,713,714],{"class":39,"line":40},[37,715,44],{"class":43},[37,717,718],{"class":39,"line":47},[37,719,720],{"class":43},"\u002F\u002F detail: show the page without waiting\n",[37,722,723,725,727,729,731,733,735,738,740,743,745,747,749,751,753,755,757,759,761,763],{"class":39,"line":222},[37,724,51],{"class":50},[37,726,55],{"class":54},[37,728,59],{"class":58},[37,730,62],{"class":54},[37,732,66],{"class":65},[37,734,565],{"class":54},[37,736,737],{"class":65},"error",[37,739,565],{"class":54},[37,741,742],{"class":65},"status",[37,744,69],{"class":54},[37,746,72],{"class":50},[37,748,364],{"class":78},[37,750,82],{"class":54},[37,752,85],{"class":50},[37,754,89],{"class":88},[37,756,92],{"class":54},[37,758,95],{"class":88},[37,760,98],{"class":54},[37,762,101],{"class":88},[37,764,765],{"class":54},", {\n",[37,767,768,771,774,776,779,781,783,785,787],{"class":39,"line":257},[37,769,770],{"class":78},"  key",[37,772,773],{"class":54},": () ",[37,775,85],{"class":50},[37,777,778],{"class":88}," `spot-${",[37,780,92],{"class":54},[37,782,95],{"class":88},[37,784,98],{"class":54},[37,786,101],{"class":88},[37,788,789],{"class":54},",\n",[37,791,792],{"class":39,"line":272},[37,793,275],{"class":54},[37,795,796,798,801,803,806,808,810,813,816,819],{"class":39,"line":591},[37,797,51],{"class":50},[37,799,800],{"class":65}," pending",[37,802,471],{"class":50},[37,804,805],{"class":78}," computed",[37,807,82],{"class":54},[37,809,85],{"class":50},[37,811,812],{"class":54}," status.value ",[37,814,815],{"class":50},"===",[37,817,818],{"class":88}," 'pending'",[37,820,104],{"class":54},[37,822,823],{"class":39,"line":615},[37,824,826],{"emptyLinePlaceholder":825},true,"\n",[37,828,829],{"class":39,"line":633},[37,830,831],{"class":43},"\u002F\u002F summary: fetch only in the browser\n",[37,833,834,836,838,840,842,845,847,849,851,854,856,858,860],{"class":39,"line":655},[37,835,51],{"class":50},[37,837,55],{"class":54},[37,839,59],{"class":58},[37,841,62],{"class":54},[37,843,844],{"class":65},"aiSummary",[37,846,565],{"class":54},[37,848,742],{"class":58},[37,850,62],{"class":54},[37,852,853],{"class":65},"summaryStatus",[37,855,69],{"class":54},[37,857,72],{"class":50},[37,859,364],{"class":78},[37,861,862],{"class":54},"(\n",[37,864,865,868,870,872,874,876,878,881],{"class":39,"line":666},[37,866,867],{"class":54},"  () ",[37,869,85],{"class":50},[37,871,89],{"class":88},[37,873,92],{"class":54},[37,875,95],{"class":88},[37,877,98],{"class":54},[37,879,880],{"class":88},"}\u002Fsummary`",[37,882,789],{"class":54},[37,884,885,888,891,893,895,898,900,902,904,906,909,912],{"class":39,"line":680},[37,886,887],{"class":54},"  { ",[37,889,890],{"class":78},"key",[37,892,773],{"class":54},[37,894,85],{"class":50},[37,896,897],{"class":88}," `spot-summary-${",[37,899,92],{"class":54},[37,901,95],{"class":88},[37,903,98],{"class":54},[37,905,101],{"class":88},[37,907,908],{"class":54},", server: ",[37,910,911],{"class":65},"false",[37,913,914],{"class":54}," },\n",[37,916,917],{"class":39,"line":686},[37,918,104],{"class":54},[116,920,921,930],{},[119,922,923],{},[122,924,925,928],{},[125,926,927],{},"Option",[125,929,130],{},[132,931,932,946],{},[122,933,934,943],{},[137,935,936,938,939,942],{},[34,937,333],{}," (= ",[34,940,941],{},"lazy: true",")",[137,944,945],{},"Doesn't wait for data when navigating within the app",[122,947,948,953],{},[137,949,950],{},[34,951,952],{},"server: false",[137,954,955,956],{},"Skips it entirely during server rendering and fetches ",[19,957,958],{},"only in the browser",[15,960,961,962,964,965,967,968,970],{},"The summary gets ",[34,963,952],{}," because of the warning above. With ",[34,966,403],{}," alone, a first visit still makes the server wait for the summary, delaying the first HTML by 1 to 2 seconds. With ",[34,969,952],{},", the server sends HTML right away without the summary, and the browser fills it in later.",[15,972,973,974,173,976,979],{},"But content fetched with ",[34,975,952],{},[19,977,978],{},"isn't in the server HTML."," Don't use it for main content that search engines must read; it fits extra information like this AI summary.",[158,981],{},[10,983,985],{"id":984},"fix-3-what-to-show-while-waiting","Fix 3: What to show while waiting",[15,987,400,988,990,991,993,994],{},[34,989,333],{},", the page appears first and ",[34,992,66],{}," is briefly empty. A blank screen there won't do, so I drew a ",[19,995,996],{},"skeleton the same size as the real layout.",[27,998,1002],{"className":999,"code":1000,"language":1001,"meta":32,"style":32},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cdiv v-if=\"error\">Couldn't load this spot.\u003C\u002Fdiv>\n\n  \u003C!-- same frame as the real screen, so nothing shifts when data arrives -->\n  \u003Cdiv v-else-if=\"pending\" class=\"skeleton\" aria-hidden=\"true\">\n    \u003Cdiv class=\"skeleton__title\" \u002F>\n    \u003Cdiv class=\"skeleton__media\" \u002F>\n    \u003Cdiv v-for=\"n in 5\" :key=\"n\" class=\"skeleton__row\" \u002F>\n  \u003C\u002Fdiv>\n\n  \u003Ctemplate v-else-if=\"spot\">\n    \u003Ch1>{{ spot.name }}\u003C\u002Fh1>\n    ...\n  \u003C\u002Ftemplate>\n\u003C\u002Ftemplate>\n","vue",[34,1003,1004,1016,1039,1043,1048,1080,1100,1117,1150,1159,1163,1182,1196,1202,1211],{"__ignoreMap":32},[37,1005,1006,1009,1013],{"class":39,"line":40},[37,1007,1008],{"class":54},"\u003C",[37,1010,1012],{"class":1011},"s9eBZ","template",[37,1014,1015],{"class":54},">\n",[37,1017,1018,1021,1024,1027,1029,1032,1035,1037],{"class":39,"line":47},[37,1019,1020],{"class":54},"  \u003C",[37,1022,1023],{"class":1011},"div",[37,1025,1026],{"class":78}," v-if",[37,1028,72],{"class":54},[37,1030,1031],{"class":88},"\"error\"",[37,1033,1034],{"class":54},">Couldn't load this spot.\u003C\u002F",[37,1036,1023],{"class":1011},[37,1038,1015],{"class":54},[37,1040,1041],{"class":39,"line":222},[37,1042,826],{"emptyLinePlaceholder":825},[37,1044,1045],{"class":39,"line":257},[37,1046,1047],{"class":43},"  \u003C!-- same frame as the real screen, so nothing shifts when data arrives -->\n",[37,1049,1050,1052,1054,1057,1059,1062,1065,1067,1070,1073,1075,1078],{"class":39,"line":272},[37,1051,1020],{"class":54},[37,1053,1023],{"class":1011},[37,1055,1056],{"class":78}," v-else-if",[37,1058,72],{"class":54},[37,1060,1061],{"class":88},"\"pending\"",[37,1063,1064],{"class":78}," class",[37,1066,72],{"class":54},[37,1068,1069],{"class":88},"\"skeleton\"",[37,1071,1072],{"class":78}," aria-hidden",[37,1074,72],{"class":54},[37,1076,1077],{"class":88},"\"true\"",[37,1079,1015],{"class":54},[37,1081,1082,1085,1087,1089,1091,1094,1098],{"class":39,"line":591},[37,1083,1084],{"class":54},"    \u003C",[37,1086,1023],{"class":1011},[37,1088,1064],{"class":78},[37,1090,72],{"class":54},[37,1092,1093],{"class":88},"\"skeleton__title\"",[37,1095,1097],{"class":1096},"s7hpK"," \u002F",[37,1099,1015],{"class":54},[37,1101,1102,1104,1106,1108,1110,1113,1115],{"class":39,"line":615},[37,1103,1084],{"class":54},[37,1105,1023],{"class":1011},[37,1107,1064],{"class":78},[37,1109,72],{"class":54},[37,1111,1112],{"class":88},"\"skeleton__media\"",[37,1114,1097],{"class":1096},[37,1116,1015],{"class":54},[37,1118,1119,1121,1123,1126,1128,1131,1134,1136,1139,1141,1143,1146,1148],{"class":39,"line":633},[37,1120,1084],{"class":54},[37,1122,1023],{"class":1011},[37,1124,1125],{"class":78}," v-for",[37,1127,72],{"class":54},[37,1129,1130],{"class":88},"\"n in 5\"",[37,1132,1133],{"class":78}," :key",[37,1135,72],{"class":54},[37,1137,1138],{"class":88},"\"n\"",[37,1140,1064],{"class":78},[37,1142,72],{"class":54},[37,1144,1145],{"class":88},"\"skeleton__row\"",[37,1147,1097],{"class":1096},[37,1149,1015],{"class":54},[37,1151,1152,1155,1157],{"class":39,"line":655},[37,1153,1154],{"class":54},"  \u003C\u002F",[37,1156,1023],{"class":1011},[37,1158,1015],{"class":54},[37,1160,1161],{"class":39,"line":666},[37,1162,826],{"emptyLinePlaceholder":825},[37,1164,1165,1167,1169,1171,1173,1176,1178,1180],{"class":39,"line":680},[37,1166,1020],{"class":54},[37,1168,1012],{"class":1011},[37,1170,1056],{"class":50},[37,1172,72],{"class":54},[37,1174,1175],{"class":88},"\"",[37,1177,66],{"class":54},[37,1179,1175],{"class":88},[37,1181,1015],{"class":54},[37,1183,1184,1186,1189,1192,1194],{"class":39,"line":686},[37,1185,1084],{"class":54},[37,1187,1188],{"class":1011},"h1",[37,1190,1191],{"class":54},">{{ spot.name }}\u003C\u002F",[37,1193,1188],{"class":1011},[37,1195,1015],{"class":54},[37,1197,1199],{"class":39,"line":1198},13,[37,1200,1201],{"class":54},"    ...\n",[37,1203,1205,1207,1209],{"class":39,"line":1204},14,[37,1206,1154],{"class":54},[37,1208,1012],{"class":1011},[37,1210,1015],{"class":54},[37,1212,1214,1217,1219],{"class":39,"line":1213},15,[37,1215,1216],{"class":54},"\u003C\u002F",[37,1218,1012],{"class":1011},[37,1220,1015],{"class":54},[15,1222,1223,1224,1227],{},"The skeleton matches the real layout's size so that text and photos ",[19,1225,1226],{},"don't jump down"," when the data arrives.",[336,1229,1231],{"id":1230},"draw-the-summary-once-after-its-final","Draw the summary once, after it's final",[15,1233,1234,1235,1238],{},"At first I wanted to show a simple rule-based summary until the AI summary arrived. But then ",[19,1236,1237],{},"the text you're reading suddenly changes to the AI summary 1 to 2 seconds later."," So while the summary loads, only a loading indicator shows, and the result is drawn once it's final. The rule-based summary is used only if the AI summary fails.",[27,1240,1242],{"className":29,"code":1241,"language":31,"meta":32,"style":32},"const summaryPending = computed(() => summaryStatus.value === 'pending')\nconst summary = computed(() => {\n  if (summaryPending.value) return '' \u002F\u002F loading: show the indicator\n  return aiSummary.value?.summary || spot.value?.summary || '' \u002F\u002F rule-based on failure\n})\n",[34,1243,1244,1268,1285,1301,1321],{"__ignoreMap":32},[37,1245,1246,1248,1251,1253,1255,1257,1259,1262,1264,1266],{"class":39,"line":40},[37,1247,51],{"class":50},[37,1249,1250],{"class":65}," summaryPending",[37,1252,471],{"class":50},[37,1254,805],{"class":78},[37,1256,82],{"class":54},[37,1258,85],{"class":50},[37,1260,1261],{"class":54}," summaryStatus.value ",[37,1263,815],{"class":50},[37,1265,818],{"class":88},[37,1267,104],{"class":54},[37,1269,1270,1272,1275,1277,1279,1281,1283],{"class":39,"line":47},[37,1271,51],{"class":50},[37,1273,1274],{"class":65}," summary",[37,1276,471],{"class":50},[37,1278,805],{"class":78},[37,1280,82],{"class":54},[37,1282,85],{"class":50},[37,1284,219],{"class":54},[37,1286,1287,1290,1293,1295,1298],{"class":39,"line":222},[37,1288,1289],{"class":50},"  if",[37,1291,1292],{"class":54}," (summaryPending.value) ",[37,1294,487],{"class":50},[37,1296,1297],{"class":88}," ''",[37,1299,1300],{"class":43}," \u002F\u002F loading: show the indicator\n",[37,1302,1303,1305,1308,1311,1314,1316,1318],{"class":39,"line":257},[37,1304,260],{"class":50},[37,1306,1307],{"class":54}," aiSummary.value?.summary ",[37,1309,1310],{"class":50},"||",[37,1312,1313],{"class":54}," spot.value?.summary ",[37,1315,1310],{"class":50},[37,1317,1297],{"class":88},[37,1319,1320],{"class":43}," \u002F\u002F rule-based on failure\n",[37,1322,1323],{"class":39,"line":272},[37,1324,275],{"class":54},[336,1326,1328],{"id":1327},"what-changes-once-await-is-gone","What changes once await is gone",[15,1330,1331,1332,1334,1335,1338,1339,1342,1343,1346,1347,1349,1350,1352,1353,1356,1357,1360],{},"Without ",[34,1333,172],{},", you can't read ",[34,1336,1337],{},"spot.value"," right away in ",[34,1340,1341],{},"setup",", because it starts empty. The original code logged the response with ",[34,1344,1345],{},"console.log(spot.value...)"," in ",[34,1348,1341],{},"; after switching to ",[34,1351,333],{},", that always logs ",[34,1354,1355],{},"undefined",". Move it into a ",[34,1358,1359],{},"watch"," so it runs when the data arrives.",[27,1362,1364],{"className":29,"code":1363,"language":31,"meta":32,"style":32},"watch(spot, (value) => {\n  if (!value) return\n  console.log('[detail] received', value)\n}, { immediate: true })\n",[34,1365,1366,1381,1395,1411],{"__ignoreMap":32},[37,1367,1368,1370,1373,1375,1377,1379],{"class":39,"line":40},[37,1369,1359],{"class":78},[37,1371,1372],{"class":54},"(spot, (",[37,1374,98],{"class":58},[37,1376,240],{"class":54},[37,1378,85],{"class":50},[37,1380,219],{"class":54},[37,1382,1383,1385,1387,1389,1392],{"class":39,"line":47},[37,1384,1289],{"class":50},[37,1386,515],{"class":54},[37,1388,599],{"class":50},[37,1390,1391],{"class":54},"value) ",[37,1393,1394],{"class":50},"return\n",[37,1396,1397,1400,1403,1405,1408],{"class":39,"line":222},[37,1398,1399],{"class":54},"  console.",[37,1401,1402],{"class":78},"log",[37,1404,208],{"class":54},[37,1406,1407],{"class":88},"'[detail] received'",[37,1409,1410],{"class":54},", value)\n",[37,1412,1413,1416,1418],{"class":39,"line":257},[37,1414,1415],{"class":54},"}, { immediate: ",[37,1417,266],{"class":65},[37,1419,397],{"class":54},[158,1421],{},[10,1423,1425],{"id":1424},"results","Results",[116,1427,1428,1441],{},[119,1429,1430],{},[122,1431,1432,1435,1438],{},[125,1433,1434],{},"Item",[125,1436,1437],{},"Before",[125,1439,1440],{},"After",[132,1442,1443,1454,1465],{},[122,1444,1445,1448,1451],{},[137,1446,1447],{},"From clicking list → detail until the screen changes",[137,1449,1450],{},"2.4s (previous screen stays)",[137,1452,1453],{},"Immediate (skeleton)",[122,1455,1456,1459,1462],{},[137,1457,1458],{},"Detail API response",[137,1460,1461],{},"Waited for the AI summary (1-2s)",[137,1463,1464],{},"0.1-0.4s",[122,1466,1467,1470,1473],{},[137,1468,1469],{},"AI summary",[137,1471,1472],{},"Arrived with the detail",[137,1474,1475],{},"Fetched separately in the browser, drawn once on arrival",[10,1477,1478],{"id":573},"Summary",[116,1480,1481,1491],{},[119,1482,1483],{},[122,1484,1485,1488],{},[125,1486,1487],{},"Situation",[125,1489,1490],{},"Use",[132,1492,1493,1503,1513,1523],{},[122,1494,1495,1498],{},[137,1496,1497],{},"The page is meaningless without the data, and the response is fast",[137,1499,1500,1502],{},[34,1501,150],{}," (the default)",[122,1504,1505,1508],{},[137,1506,1507],{},"The response may be slow, and you want to show the screen first",[137,1509,1510,1512],{},[34,1511,333],{}," + loading\u002Fskeleton",[122,1514,1515,1518],{},[137,1516,1517],{},"Slow extra information (AI summaries, recommendations, stats)",[137,1519,1520,1521],{},"A separate API + ",[34,1522,952],{},[122,1524,1525,1528],{},[137,1526,1527],{},"Main content search engines must read",[137,1529,1530,1531],{},"Don't use ",[34,1532,952],{},[15,1534,1535,1537,1538,1541,1542,1544],{},[34,1536,150],{}," is the simplest code, so it's easy to default to. But know that ",[19,1539,1540],{},"the moment the response is slow, navigation freezes for exactly that long."," First pull the slow part out of the response, then make the remaining wait visible to users with ",[34,1543,403],{}," and a skeleton.",[15,1546,1547,1548,1346,1550,1552,1553,1556,1557,95],{},"Problems caused by a wrong ",[34,1549,890],{},[34,1551,176],{},"\u002F",[34,1554,1555],{},"useAsyncData"," are covered in ",[1558,1559,1561],"a",{"href":1560},"\u002Fen\u002Fnuxt-useasyncdata-cache-error","Fixing Nuxt useAsyncData caching with dynamic keys",[10,1563,1565],{"id":1564},"references","References",[1567,1568,1569,1578,1585],"ul",{},[1570,1571,1572],"li",{},[1558,1573,1577],{"href":1574,"rel":1575},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fcomposables\u002Fuse-fetch",[1576],"nofollow","Nuxt docs: useFetch",[1570,1579,1580],{},[1558,1581,1584],{"href":1582,"rel":1583},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fcomposables\u002Fuse-lazy-fetch",[1576],"Nuxt docs: useLazyFetch",[1570,1586,1587],{},[1558,1588,1591],{"href":1589,"rel":1590},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fdata-fetching",[1576],"Nuxt docs: Data fetching (lazy, server options)",[1593,1594,1595],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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}",{"title":32,"searchDepth":47,"depth":47,"links":1597},[1598,1599,1602,1603,1604,1608,1609,1610],{"id":12,"depth":47,"text":13},{"id":162,"depth":47,"text":163,"children":1600},[1601],{"id":338,"depth":222,"text":339},{"id":438,"depth":47,"text":439},{"id":705,"depth":47,"text":706},{"id":984,"depth":47,"text":985,"children":1605},[1606,1607],{"id":1230,"depth":222,"text":1231},{"id":1327,"depth":222,"text":1328},{"id":1424,"depth":47,"text":1425},{"id":573,"depth":47,"text":1478},{"id":1564,"depth":47,"text":1565},"Clicking from a list to a detail page left the screen unchanged for 2.4 seconds. The cause: await useFetch plus a slow AI call inside the response. How useLazyFetch, server: false, and a skeleton fixed it, with measured results.","md",{"date":1614,"field":1615,"tags":1616},"2026.10.19","frontend",[1617,1618],"nuxt","web-performance","\u002Fen\u002Fnuxt-usefetch-await-blocks-navigation",{"title":5,"description":1611},{"loc":1619},"en\u002Fnuxt-usefetch-await-blocks-navigation","k9Eh706VVk1ggTi4hLKltof68u4HY-pxrRd-1hAA2OY",1792407000536]