[{"data":1,"prerenderedAt":1083},["ShallowReactive",2],{"content:\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank":3,"surroundings:\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank":1075},{"id":4,"title":5,"body":6,"description":1056,"extension":1057,"meta":1058,"navigation":1069,"path":1070,"seo":1071,"stem":1073,"__hash__":1074},"content\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank\u002Findex.md","Avoiding Main-Thread Image Decode Jank",{"type":7,"value":8,"toc":1039},"minimark",[9,14,29,37,137,142,175,179,185,191,197,203,295,299,304,312,316,459,462,466,477,681,685,692,823,827,830,834,842,846,849,853,879,883,899,905,914,920,924,936,945,954,967,979,988,997,1001,1024,1029,1032,1035],[10,11,13],"h1",{"id":12},"how-to-avoid-main-thread-image-decode-jank","How to Avoid Main-Thread Image Decode Jank",[15,16,17,18,23,24,28],"p",{},"This guide is part of ",[19,20,22],"a",{"href":21},"\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002F","Image Decoding & Placeholders"," in ",[19,25,27],{"href":26},"\u002Fimage-media-optimization\u002F","Image & Media Optimization",". Modern browsers decode most images off the main thread, on dedicated decoder or raster threads. But several common patterns pull decoding back onto the main thread or into the critical path of a frame: drawing images to canvas, inserting large images and painting them in the same frame, image-processing libraries running in JavaScript, and oversized images whose decode is too big to finish in the background before the frame needs it.",[15,30,31,32,36],{},"When that happens, the main thread is blocked for tens of milliseconds per image. During scrolling, frames are dropped; during interactions, the decode becomes part of processing or presentation delay and pushes INP past ",[33,34,35],"code",{},"200ms",". On image-heavy experiences — galleries, maps, editors, social feeds — decode jank is often the dominant performance problem on mid-tier phones.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,71,47,77,47,85,47,92,47,96,47,102,47,109,47,113,47,116,47,119,47,123,47,126,47,130,47,134,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 280","100%","img","Common code paths that cause image decoding to block the main thread.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Paths that put decoding on the main thread",[60,61,45],"desc",{},[49,63],{"x":64,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"1","758","278","10","none","currentColor","stroke-opacity:0.18",[72,73,58],"text",{"x":74,"y":75,"fill":69,"style":76},"28.0","34.0","font-size:16px;font-weight:700",[49,78],{"x":74,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":83,"style":84},"56.0","704.0","52.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[72,86,91],{"x":87,"y":88,"fill":69,"style":89,"textAnchor":90},"42.0","78.0","font-size:13px;font-weight:700","start","canvas drawImage of undecoded",[72,93,95],{"x":87,"y":94,"fill":69,"style":89,"textAnchor":90},"95.0","image",[72,97,101],{"x":98,"y":99,"fill":69,"style":100,"textAnchor":90},"278.0","86.5","font-size:12px","synchronous decode on the calling thread",[49,103],{"x":74,"y":104,"width":80,"height":105,"rx":82,"fill":106,"stroke":107,"style":108},"118.0","40.0","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[72,110,112],{"x":87,"y":111,"fill":69,"style":89,"textAnchor":90},"142.5","Insert + paint in the same frame",[72,114,115],{"x":98,"y":111,"fill":69,"style":100,"textAnchor":90},"large images may decode synchronously to avoid a blank",[49,117],{"x":74,"y":118,"width":80,"height":105,"rx":82,"fill":106,"stroke":107,"style":108},"168.0",[72,120,122],{"x":87,"y":121,"fill":69,"style":89,"textAnchor":90},"192.5","JS image processing",[72,124,125],{"x":98,"y":121,"fill":69,"style":100,"textAnchor":90},"resizing, filters, EXIF parsing in main-thread JavaScript",[49,127],{"x":74,"y":128,"width":80,"height":105,"rx":82,"fill":69,"stroke":69,"style":129},"218.0","fill-opacity:0.06;stroke-opacity:0.4",[72,131,133],{"x":87,"y":132,"fill":69,"style":89,"textAnchor":90},"242.5","Oversized images",[72,135,136],{"x":98,"y":132,"fill":69,"style":100,"textAnchor":90},"decode too large to finish off-thread before needed",[138,139,141],"h2",{"id":140},"rapid-diagnosis","Rapid Diagnosis",[143,144,145,153,159,169],"ul",{},[146,147,148,152],"li",{},[149,150,151],"strong",{},"Find Image Decode events on the Main track"," in a throttled trace; their duration and the URL tell you which images.",[146,154,155,158],{},[149,156,157],{},"Check scroll performance."," Record scrolling through image-heavy content with the frame track visible; dropped frames coinciding with image decodes confirm the issue.",[146,160,161,164,165,168],{},[149,162,163],{},"Check canvas code."," Search for ",[33,166,167],{},"drawImage"," with freshly loaded images.",[146,170,171,174],{},[149,172,173],{},"Check client-side processing."," Uploader previews, cropping tools and EXIF orientation fixes often run in JavaScript on the main thread.",[138,176,178],{"id":177},"root-cause-analysis","Root Cause Analysis",[15,180,181,184],{},[149,182,183],{},"1. Canvas drawImage before decode."," The browser must decode synchronously to draw the pixels.",[15,186,187,190],{},[149,188,189],{},"2. Oversized sources."," A 4000px image displayed at 400px requires decoding 100x the needed pixels.",[15,192,193,196],{},[149,194,195],{},"3. Bulk insertion."," Inserting dozens of images at once (a grid page loaded by \"load more\") schedules many decodes simultaneously.",[15,198,199,202],{},[149,200,201],{},"4. Main-thread image libraries."," Resizing or filtering images in JavaScript blocks the thread for the whole operation.",[15,204,205],{},[40,206,47,209,47,212,47,215,47,217,47,220,47,222,47,232,47,239,47,246,47,251,47,255,47,259,47,263,47,267,47,272,47,276,47,280,47,284,47,288,47],{"viewBox":207,"width":43,"role":44,"ariaLabel":208,"style":46},"0 0 760 216","Bar chart of main-thread time when appending 24 photos to a grid under different strategies.",[49,210],{"className":211,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,213,214],{},"Main-thread time in a photo grid \"load more\" (mid-tier phone)",[60,216,208],{},[49,218],{"x":64,"y":64,"width":65,"height":219,"rx":67,"fill":68,"stroke":69,"style":70},"214",[72,221,214],{"x":74,"y":75,"fill":69,"style":76},[223,224],"line",{"x1":225,"y1":226,"x2":225,"y2":227,"stroke":107,"strokeWidth":228,"strokeDashArray":229,"style":231},"290.8","72.0","194.0","1.5",[230,230],"4","stroke-opacity:0.9",[72,233,238],{"x":234,"y":235,"fill":69,"style":236,"textAnchor":237},"227.8","92.0","font-size:13px","end","Full-size images appended at once",[49,240],{"x":241,"y":88,"width":242,"height":243,"rx":244,"fill":106,"stroke":107,"style":245},"239.8","428.2","19","3","fill-opacity:0.7;stroke-opacity:0.9",[72,247,250],{"x":248,"y":235,"fill":69,"style":249},"674.0","font-size:12px;font-weight:600","420ms",[72,252,254],{"x":234,"y":253,"fill":69,"style":236,"textAnchor":237},"123.0","Right-sized thumbnails",[49,256],{"x":241,"y":257,"width":258,"height":243,"rx":244,"fill":106,"stroke":107,"style":245},"109.0","96.9",[72,260,262],{"x":261,"y":253,"fill":69,"style":249},"342.6","95ms",[72,264,266],{"x":234,"y":265,"fill":69,"style":236,"textAnchor":237},"154.0","+ decoding=async",[49,268],{"x":241,"y":269,"width":270,"height":243,"rx":244,"fill":83,"stroke":83,"style":271},"140.0","30.6","fill-opacity:0.55;stroke-opacity:0.9",[72,273,275],{"x":274,"y":265,"fill":69,"style":249},"276.4","30ms",[72,277,279],{"x":234,"y":278,"fill":69,"style":236,"textAnchor":237},"185.0","+ staggered insertion",[49,281],{"x":241,"y":282,"width":283,"height":243,"rx":244,"fill":83,"stroke":83,"style":271},"171.0","12.2",[72,285,287],{"x":286,"y":278,"fill":69,"style":249},"258.0","12ms",[72,289,294],{"x":225,"y":290,"fill":291,"style":292,"textAnchor":293},"68.0","#8a6608","font-size:12px;font-weight:700","middle","50ms long task",[138,296,298],{"id":297},"step-by-step-resolution","Step-by-Step Resolution",[300,301,303],"h3",{"id":302},"_1-serve-images-at-display-size","1. Serve images at display size",[15,305,306,307,311],{},"Generate thumbnails and responsive sizes so decode work matches what is displayed — the single largest reduction, as covered in ",[19,308,310],{"href":309},"\u002Fimage-media-optimization\u002Fresponsive-images-with-srcset-and-sizes\u002F","responsive images with srcset and sizes",".",[300,313,315],{"id":314},"_2-decode-off-thread-for-canvas-and-processing","2. Decode off-thread for canvas and processing",[317,318,323],"pre",{"className":319,"code":320,"language":321,"meta":322,"style":322},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Decode off the main thread, then draw the bitmap (no sync decode).\nconst blob = await fetch(url).then((r) => r.blob());\nconst bitmap = await createImageBitmap(blob, { resizeWidth: 800, resizeQuality: 'high' });\nctx.drawImage(bitmap, 0, 0);\nbitmap.close();\n\u002F\u002F trade-off: createImageBitmap with resize options produces a scaled bitmap;\n\u002F\u002F keep the original blob if you need full resolution later.\n","javascript","",[33,324,325,333,383,414,435,447,453],{"__ignoreMap":322},[326,327,329],"span",{"class":223,"line":328},1,[326,330,332],{"class":331},"sjfSM","\u002F\u002F Decode off the main thread, then draw the bitmap (no sync decode).\n",[326,334,336,340,344,347,350,354,358,361,364,368,371,374,377,380],{"class":223,"line":335},2,[326,337,339],{"class":338},"sPARh","const",[326,341,343],{"class":342},"sPXB4"," blob",[326,345,346],{"class":338}," =",[326,348,349],{"class":338}," await",[326,351,353],{"class":352},"smZ65"," fetch",[326,355,357],{"class":356},"saISM","(url).",[326,359,360],{"class":352},"then",[326,362,363],{"class":356},"((",[326,365,367],{"class":366},"sQw3B","r",[326,369,370],{"class":356},") ",[326,372,373],{"class":338},"=>",[326,375,376],{"class":356}," r.",[326,378,379],{"class":352},"blob",[326,381,382],{"class":356},"());\n",[326,384,386,388,391,393,395,398,401,404,407,411],{"class":223,"line":385},3,[326,387,339],{"class":338},[326,389,390],{"class":342}," bitmap",[326,392,346],{"class":338},[326,394,349],{"class":338},[326,396,397],{"class":352}," createImageBitmap",[326,399,400],{"class":356},"(blob, { resizeWidth: ",[326,402,403],{"class":342},"800",[326,405,406],{"class":356},", resizeQuality: ",[326,408,410],{"class":409},"sZ8jY","'high'",[326,412,413],{"class":356}," });\n",[326,415,417,420,422,425,427,430,432],{"class":223,"line":416},4,[326,418,419],{"class":356},"ctx.",[326,421,167],{"class":352},[326,423,424],{"class":356},"(bitmap, ",[326,426,53],{"class":342},[326,428,429],{"class":356},", ",[326,431,53],{"class":342},[326,433,434],{"class":356},");\n",[326,436,438,441,444],{"class":223,"line":437},5,[326,439,440],{"class":356},"bitmap.",[326,442,443],{"class":352},"close",[326,445,446],{"class":356},"();\n",[326,448,450],{"class":223,"line":449},6,[326,451,452],{"class":331},"\u002F\u002F trade-off: createImageBitmap with resize options produces a scaled bitmap;\n",[326,454,456],{"class":223,"line":455},7,[326,457,458],{"class":331},"\u002F\u002F keep the original blob if you need full resolution later.\n",[15,460,461],{},"Expected outcome: canvas drawing no longer includes decode time on the main thread.",[300,463,465],{"id":464},"_3-move-image-processing-into-a-worker","3. Move image processing into a worker",[15,467,468,469,472,473,476],{},"Use ",[33,470,471],{},"OffscreenCanvas"," and ",[33,474,475],{},"createImageBitmap"," in a Web Worker for resizing, cropping or filtering uploads, and transfer the result back.",[317,478,480],{"className":319,"code":479,"language":321,"meta":322,"style":322},"\u002F\u002F worker.js\nself.onmessage = async ({ data: { blob, width } }) => {\n  const bmp = await createImageBitmap(blob);\n  const scale = width \u002F bmp.width;\n  const canvas = new OffscreenCanvas(width, Math.round(bmp.height * scale));\n  canvas.getContext('2d').drawImage(bmp, 0, 0, canvas.width, canvas.height);\n  const out = await canvas.convertToBlob({ type: 'image\u002Fwebp', quality: 0.82 });\n  self.postMessage(out);\n};\n\u002F\u002F trade-off: OffscreenCanvas support is broad in modern browsers, but test the\n\u002F\u002F fallback path for older Safari versions if you support them.\n",[33,481,482,487,524,541,559,589,620,651,663,669,675],{"__ignoreMap":322},[326,483,484],{"class":223,"line":328},[326,485,486],{"class":331},"\u002F\u002F worker.js\n",[326,488,489,492,495,497,500,503,506,509,511,513,516,519,521],{"class":223,"line":335},[326,490,491],{"class":356},"self.",[326,493,494],{"class":352},"onmessage",[326,496,346],{"class":338},[326,498,499],{"class":338}," async",[326,501,502],{"class":356}," ({ ",[326,504,505],{"class":366},"data",[326,507,508],{"class":356},": { ",[326,510,379],{"class":366},[326,512,429],{"class":356},[326,514,515],{"class":366},"width",[326,517,518],{"class":356}," } }) ",[326,520,373],{"class":338},[326,522,523],{"class":356}," {\n",[326,525,526,529,532,534,536,538],{"class":223,"line":385},[326,527,528],{"class":338},"  const",[326,530,531],{"class":342}," bmp",[326,533,346],{"class":338},[326,535,349],{"class":338},[326,537,397],{"class":352},[326,539,540],{"class":356},"(blob);\n",[326,542,543,545,548,550,553,556],{"class":223,"line":416},[326,544,528],{"class":338},[326,546,547],{"class":342}," scale",[326,549,346],{"class":338},[326,551,552],{"class":356}," width ",[326,554,555],{"class":338},"\u002F",[326,557,558],{"class":356}," bmp.width;\n",[326,560,561,563,566,568,571,574,577,580,583,586],{"class":223,"line":437},[326,562,528],{"class":338},[326,564,565],{"class":342}," canvas",[326,567,346],{"class":338},[326,569,570],{"class":338}," new",[326,572,573],{"class":352}," OffscreenCanvas",[326,575,576],{"class":356},"(width, Math.",[326,578,579],{"class":352},"round",[326,581,582],{"class":356},"(bmp.height ",[326,584,585],{"class":338},"*",[326,587,588],{"class":356}," scale));\n",[326,590,591,594,597,600,603,606,608,611,613,615,617],{"class":223,"line":449},[326,592,593],{"class":356},"  canvas.",[326,595,596],{"class":352},"getContext",[326,598,599],{"class":356},"(",[326,601,602],{"class":409},"'2d'",[326,604,605],{"class":356},").",[326,607,167],{"class":352},[326,609,610],{"class":356},"(bmp, ",[326,612,53],{"class":342},[326,614,429],{"class":356},[326,616,53],{"class":342},[326,618,619],{"class":356},", canvas.width, canvas.height);\n",[326,621,622,624,627,629,631,634,637,640,643,646,649],{"class":223,"line":455},[326,623,528],{"class":338},[326,625,626],{"class":342}," out",[326,628,346],{"class":338},[326,630,349],{"class":338},[326,632,633],{"class":356}," canvas.",[326,635,636],{"class":352},"convertToBlob",[326,638,639],{"class":356},"({ type: ",[326,641,642],{"class":409},"'image\u002Fwebp'",[326,644,645],{"class":356},", quality: ",[326,647,648],{"class":342},"0.82",[326,650,413],{"class":356},[326,652,654,657,660],{"class":223,"line":653},8,[326,655,656],{"class":356},"  self.",[326,658,659],{"class":352},"postMessage",[326,661,662],{"class":356},"(out);\n",[326,664,666],{"class":223,"line":665},9,[326,667,668],{"class":356},"};\n",[326,670,672],{"class":223,"line":671},10,[326,673,674],{"class":331},"\u002F\u002F trade-off: OffscreenCanvas support is broad in modern browsers, but test the\n",[326,676,678],{"class":223,"line":677},11,[326,679,680],{"class":331},"\u002F\u002F fallback path for older Safari versions if you support them.\n",[300,682,684],{"id":683},"_4-stagger-bulk-insertions","4. Stagger bulk insertions",[15,686,687,688,691],{},"When appending many images, insert them in small batches across frames, with ",[33,689,690],{},"decoding=\"async\"",", rather than all at once.",[15,693,694],{},[40,695,47,698,47,701,47,704,47,706,47,723,47,726,47,728,47,732,47,736,47,739,47,744,47,751,47,757,47,761,47,767,47,770,47,774,47,776,47,779,47,782,47,785,47,788,47,791,47,794,47,798,47,800,47,803,47,806,47,809,47,813,47,816,47,819,47],{"viewBox":696,"width":43,"role":44,"ariaLabel":697,"style":46},"0 0 760 325","Decision sequence for removing main-thread decode cost depending on how images are used.",[49,699],{"className":700,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,702,703],{},"Fixing a decode-heavy code path",[60,705,697],{},[707,708,709],"defs",{},[710,711,718],"marker",{"id":712,"viewBox":713,"refX":714,"refY":715,"markerWidth":716,"markerHeight":716,"orient":717},"fa87a5fd0e","0 0 10 10","9","5","7","auto-start-reverse",[719,720],"path",{"d":721,"fill":69,"style":722},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[49,724],{"x":64,"y":64,"width":65,"height":725,"rx":67,"fill":68,"stroke":69,"style":70},"323",[72,727,703],{"x":74,"y":75,"fill":69,"style":76},[49,729],{"x":74,"y":79,"width":730,"height":731,"rx":82,"fill":69,"stroke":69,"style":129},"320.0","35.0",[72,733,735],{"x":734,"y":88,"fill":69,"style":89,"textAnchor":293},"188.0","Images larger than displayed?",[49,737],{"x":738,"y":79,"width":730,"height":731,"rx":82,"fill":83,"stroke":83,"style":84},"412.0",[72,740,743],{"x":741,"y":88,"fill":69,"style":742,"textAnchor":293},"572.0","font-size:12.5px","Serve right-sized versions first",[223,745],{"x1":746,"y1":747,"x2":748,"y2":747,"stroke":69,"strokeWidth":228,"style":749,"markerEnd":750},"348.0","73.5","410.0","stroke-opacity:0.6","url(#fa87a5fd0e)",[72,752,756],{"x":753,"y":754,"fill":83,"style":755,"textAnchor":293},"380.0","66.5","font-size:11.5px;font-weight:700","yes",[223,758],{"x1":734,"y1":759,"x2":734,"y2":760,"stroke":69,"strokeWidth":228,"style":749,"markerEnd":750},"91.0","125.0",[72,762,766],{"x":763,"y":764,"fill":765,"style":755},"196.0","113.0","#51617a","no",[49,768],{"x":74,"y":769,"width":730,"height":731,"rx":82,"fill":69,"stroke":69,"style":129},"127.0",[72,771,773],{"x":734,"y":772,"fill":69,"style":89,"textAnchor":293},"149.0","Drawn to canvas?",[49,775],{"x":738,"y":769,"width":730,"height":731,"rx":82,"fill":83,"stroke":83,"style":84},[72,777,778],{"x":741,"y":772,"fill":69,"style":742,"textAnchor":293},"createImageBitmap before drawImage",[223,780],{"x1":746,"y1":781,"x2":748,"y2":781,"stroke":69,"strokeWidth":228,"style":749,"markerEnd":750},"144.5",[72,783,756],{"x":753,"y":784,"fill":83,"style":755,"textAnchor":293},"137.5",[223,786],{"x1":734,"y1":787,"x2":734,"y2":763,"stroke":69,"strokeWidth":228,"style":749,"markerEnd":750},"162.0",[72,789,766],{"x":763,"y":790,"fill":765,"style":755},"184.0",[49,792],{"x":74,"y":793,"width":730,"height":731,"rx":82,"fill":69,"stroke":69,"style":129},"198.0",[72,795,797],{"x":734,"y":796,"fill":69,"style":89,"textAnchor":293},"220.0","Processed in JavaScript?",[49,799],{"x":738,"y":793,"width":730,"height":731,"rx":82,"fill":83,"stroke":83,"style":84},[72,801,802],{"x":741,"y":796,"fill":69,"style":742,"textAnchor":293},"Move to a worker with OffscreenCanvas",[223,804],{"x1":746,"y1":805,"x2":748,"y2":805,"stroke":69,"strokeWidth":228,"style":749,"markerEnd":750},"215.5",[72,807,756],{"x":753,"y":808,"fill":83,"style":755,"textAnchor":293},"208.5",[223,810],{"x1":734,"y1":811,"x2":734,"y2":812,"stroke":69,"strokeWidth":228,"style":749,"markerEnd":750},"233.0","267.0",[72,814,766],{"x":763,"y":815,"fill":765,"style":755},"255.0",[49,817],{"x":74,"y":818,"width":730,"height":75,"rx":82,"fill":106,"stroke":107,"style":108},"269.0",[72,820,822],{"x":734,"y":821,"fill":69,"style":742,"textAnchor":293},"290.5","decoding=async and staggered insertion",[138,824,826],{"id":825},"verification","Verification",[15,828,829],{},"Re-record scrolling and the triggering interaction: Image Decode events should move off the Main track (or shrink to a few milliseconds), dropped frames should disappear, and the interaction's long tasks should be gone. In the field, INP for image-heavy interactions and LoAF render-phase durations on those views should improve.",[138,831,833],{"id":832},"worked-example-an-image-upload-preview","Worked Example: An Image Upload Preview",[15,835,836,837,472,839,841],{},"A marketplace's listing form showed previews of uploaded photos (often 12-megapixel phone images) by reading them with FileReader, drawing them to a canvas to fix orientation and resize, and displaying the result. Each photo blocked the main thread for 300–600ms on mid-tier phones, and adding five photos made the form unresponsive for seconds. Moving decode and resize into a worker with ",[33,838,475],{},[33,840,471],{},", and showing a skeleton until each preview was ready, removed the long tasks; INP for the form's interactions dropped from 640ms to 110ms.",[138,843,845],{"id":844},"scrolling-image-feeds","Scrolling Image Feeds",[15,847,848],{},"Infinite feeds stress decoding continuously. Beyond right-sizing and async decoding, three practices keep them smooth: limit the number of images in the DOM by virtualising the list (off-screen images are removed and their bitmaps released); load the next batch's images slightly ahead of the viewport so decoding happens before they are needed; and avoid CSS effects (filters, large shadows) on every feed image, which add paint cost on top of decode. Measure with a scripted scroll in a throttled trace and count dropped frames before and after each change.",[138,850,852],{"id":851},"common-mistakes","Common Mistakes",[143,854,855,861,867,873],{},[146,856,857,860],{},[149,858,859],{},"Drawing freshly loaded images to canvas."," Forces synchronous decode.",[146,862,863,866],{},[149,864,865],{},"Using full-resolution originals as thumbnails."," Decode cost scales with source pixels.",[146,868,869,872],{},[149,870,871],{},"Processing uploads on the main thread."," Blocks the UI for seconds on phones.",[146,874,875,878],{},[149,876,877],{},"Appending large batches at once."," Concentrates decodes into one long frame.",[138,880,882],{"id":881},"edge-cases","Edge Cases",[15,884,885,888,889,472,892,894,895,898],{},[149,886,887],{},"EXIF orientation."," Modern browsers apply EXIF orientation automatically in ",[33,890,891],{},"\u003Cimg>",[33,893,475],{}," (with ",[33,896,897],{},"imageOrientation: 'from-image'","), so manual canvas rotation is often unnecessary.",[15,900,901,904],{},[149,902,903],{},"HEIC uploads."," Many browsers cannot decode HEIC; server-side conversion avoids heavy JavaScript decoders.",[15,906,907,910,911,913],{},[149,908,909],{},"WebGL textures."," Uploading large textures stalls the GPU thread; use ",[33,912,475],{}," and appropriately sized textures.",[15,915,916,919],{},[149,917,918],{},"Memory limits."," Decoding very large images can fail or crash tabs on low-memory devices; resize on the server whenever possible.",[138,921,923],{"id":922},"faq","FAQ",[925,926,929,933],"details",{"className":927},[928],"faq-item",[930,931,932],"summary",{},"Do browsers ever decode images on the main thread for normal img elements?",[15,934,935],{},"Usually not, but synchronous decodes can occur when an image must be painted immediately (for example, inserted and painted in the same frame) or for very large images. Traces show where it happens.",[925,937,939,942],{"className":938},[928],[930,940,941],{},"Is createImageBitmap always off the main thread?",[15,943,944],{},"It is designed to decode asynchronously, and browsers implement it off the main thread for common formats. Calling it in a worker guarantees the work is away from the UI thread.",[925,946,948,951],{"className":947},[928],[930,949,950],{},"Does image format affect decode jank?",[15,952,953],{},"Yes: AVIF and JPEG XL decode more slowly per pixel than JPEG. Right-sizing usually matters more than format; for huge images on low-end devices, measure.",[925,955,957,960],{"className":956},[928],[930,958,959],{},"Does decoding affect INP even if it is off the main thread?",[15,961,962,963,966],{},"If an interaction reveals an image that is not yet decoded, the browser may delay the frame (presentation delay) or show a blank first. Pre-decode with ",[33,964,965],{},"decode()"," before revealing.",[925,968,970,973],{"className":969},[928],[930,971,972],{},"Can service workers decode images?",[15,974,975,976,978],{},"Service workers can use ",[33,977,475],{}," and OffscreenCanvas in some browsers, but for processing, a dedicated worker is the more common and better-supported choice.",[925,980,982,985],{"className":981},[928],[930,983,984],{},"How many images per frame is safe to insert?",[15,986,987],{},"It depends on size and device. With right-sized thumbnails and async decoding, a few per frame is typically fine on mid-tier phones; measure frame durations while tuning batch sizes.",[925,989,991,994],{"className":990},[928],[930,992,993],{},"Can I detect decode jank in the field?",[15,995,996],{},"Long Animation Frames help: frames during scroll or interactions with long render phases on image-heavy views suggest decode or paint cost. Combine with Element Timing or image load events to correlate frames with image arrivals, and segment by device class — decode jank is concentrated on low-end hardware.",[138,998,1000],{"id":999},"related","Related",[143,1002,1003,1010,1017],{},[146,1004,1005,1009],{},[19,1006,1008],{"href":1007},"\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Fusing-decoding-async-and-the-decode-api\u002F","Using decoding=async and the decode() API"," — the declarative controls.",[146,1011,1012,1016],{},[19,1013,1015],{"href":1014},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002F","Transferring ArrayBuffers to workers without copying"," — moving image data cheaply.",[146,1018,1019,1023],{},[19,1020,1022],{"href":1021},"\u002Frendering-css-performance\u002Fdom-size-and-list-virtualization\u002Fvirtualizing-long-lists-in-react\u002F","Virtualizing long lists in React"," — keeping image feeds' DOM small.",[1025,1026,1028],"script",{"type":1027},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Avoid Main-Thread Image Decode Jank\",\n  \"description\": \"How to find image decoding on the main thread, why it happens, and how to move or shrink it so scrolling and interactions stay smooth.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Serve images at display size\",\n      \"text\": \"Generate thumbnails and responsive sizes so decode work matches what is displayed — the single largest reduction, as covered in responsive images with srcset and sizes.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Decode off-thread for canvas and processing\",\n      \"text\": \"Expected outcome: canvas drawing no longer includes decode time on the main thread.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Move image processing into a worker\",\n      \"text\": \"Use OffscreenCanvas and createImageBitmap in a Web Worker for resizing, cropping or filtering uploads, and transfer the result back.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Stagger bulk insertions\",\n      \"text\": \"When appending many images, insert them in small batches across frames, with decoding=\\\"async\\\", rather than all at once.\"\n    }\n  ]\n}\n",[1025,1030,1031],{"type":1027},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Avoid Main-Thread Image Decode Jank\",\n  \"description\": \"How to find image decoding on the main thread, why it happens, and how to move or shrink it so scrolling and interactions stay smooth.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank\u002F\"\n  }\n}\n",[1025,1033,1034],{"type":1027},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Image & Media Optimization\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Image Decoding & Placeholders\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Avoiding Main-Thread Image Decode Jank\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank\u002F\"\n    }\n  ]\n}\n",[1036,1037,1038],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":322,"searchDepth":335,"depth":335,"links":1040},[1041,1042,1043,1049,1050,1051,1052,1053,1054,1055],{"id":140,"depth":335,"text":141},{"id":177,"depth":335,"text":178},{"id":297,"depth":335,"text":298,"children":1044},[1045,1046,1047,1048],{"id":302,"depth":385,"text":303},{"id":314,"depth":385,"text":315},{"id":464,"depth":385,"text":465},{"id":683,"depth":385,"text":684},{"id":825,"depth":335,"text":826},{"id":832,"depth":335,"text":833},{"id":844,"depth":335,"text":845},{"id":851,"depth":335,"text":852},{"id":881,"depth":335,"text":882},{"id":922,"depth":335,"text":923},{"id":999,"depth":335,"text":1000},"How to find image decoding on the main thread, why it happens, and how to move or shrink it so scrolling and interactions stay smooth.","md",{"slug":1059,"type":1060,"breadcrumb":1061,"datePublished":1068,"dateModified":1068},"avoiding-main-thread-image-decode-jank","article",[1062,1064,1065,1066],{"name":1063,"url":555},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1067},"\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank\u002F","2026-10-06",true,"\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank",{"title":5,"description":1072},"Large image decodes on the main thread cause scroll jank and slow interactions. Right-size images, decode off-thread with createImageBitmap, and stagger work.","image-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank\u002Findex","xbpuUb8kcDb8xU-o2rO1SwJL_yuyyi6fg--eSzzcZ-A",[1076,1079],{"title":22,"path":1077,"stem":1078,"children":-1},"\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders","image-media-optimization\u002Fimage-decoding-and-placeholders\u002Findex",{"title":1080,"path":1081,"stem":1082,"children":-1},"CSS Background Images as LCP Elements","\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Fcss-background-images-as-lcp-elements","image-media-optimization\u002Fimage-decoding-and-placeholders\u002Fcss-background-images-as-lcp-elements\u002Findex",1791308077254]