Setting the file. One moment.
Unoptimized Image · Vercel Optimize · vercel-labs/agent-skills · Skills Docs
ContentsBack to the top of the page lib/scanners/unoptimized-image.mjs
lib/scanners/ unoptimized-image.mjs
JavaScript · 113 lines · 4 KB
11
description:
12 'Four shapes of image-cost waste: raw <img> tags bypass the framework Image component; `images.unoptimized: true` disables Vercel image optimization globally; <Image fill> without `sizes` forces serving the largest source variant; <Image src=".svg"> without `unoptimized` routes vector data through the raster pipeline.' ,
13 fix:
14 'For raw <img>: switch to next/image, enhanced-img (SvelteKit), <Image /> (Astro), or NuxtImg. For global unoptimized:true: remove the flag unless the project is hosted outside Vercel. For fill without sizes: add `sizes="(max-width: 768px) 100vw, 50vw"` or whatever matches your layout. For SVG: add `unoptimized` so the raw SVG ships instead of rastering it.' ,
15 citations: [
16 'https://nextjs.org/docs/app/api-reference/components/image' ,
17 'https://vercel.com/docs/image-optimization' ,
18 ],
19 excludeGlobs: [ 'node_modules/**' , '.next/**' , 'dist/**' , '__tests__/**' , 'cypress/**' , '*.test.*' ],
20 includeGlobs: [ '**/*.{tsx,jsx,html,svelte,astro,vue,js,mjs,ts}' ],
21 };
22
23 const IMG_RE = /<img \s + [ ^ >] * src \s * = \s * ["'{`] / g ;
24 const GLOBAL_UNOPT_RE = /images \s * : \s * \{ [ ^ }] *\b unoptimized \s * : \s * true/ ;
25 const IMAGE_TAG_RE = /<Image \b [ ^ >] *? \/ ? >/ g ;
26 const NEXT_IMAGE_IMPORT_RE = /from \s + ['"] next \/ image ['"] / ;
27
28 export function scan ({ files }) {
29 const out = [];
30 for ( const { path , content } of files) {
31 if ( isJsxLike (path)) {
32 let m;
33 IMG_RE .lastIndex = 0 ;
34 while ((m = IMG_RE . exec (content)) !== null ) {
35 out. push ({
36 pattern: metadata.id,
37 subtype: 'raw-img' ,
38 file: path,
39 line: lineOf (content, m.index),
40 evidence: snippet (content, m.index),
41 trafficIndependent: metadata.trafficIndependent,
42 });
43 }
44 }
45
46 if ( isNextConfig (path)) {
47 const match = GLOBAL_UNOPT_RE . exec (content);
48 if (match) {
49 out. push ({
50 pattern: metadata.id,
51 subtype: 'global-unoptimized' ,
52 file: path,
53 line: lineOf (content, match.index),
54 evidence: 'images: { unoptimized: true } — disables Vercel image optimization for the entire project' ,
55 // Config-level flag affects every image regardless of route.
56 trafficIndependent: true ,
57 });
58 }
59 }
60
61 // Only fire if next/image is imported — otherwise `Image` is some
62 // other component.
63 if ( isJsxLike (path) && NEXT_IMAGE_IMPORT_RE . test (content)) {
64 let m;
65 IMAGE_TAG_RE .lastIndex = 0 ;
66 while ((m = IMAGE_TAG_RE . exec (content)) !== null ) {
67 const tag = m[ 0 ];
68 const hasFill = / \b fill \b / . test (tag);
69 const hasSizes = / \b sizes \s * =/ . test (tag);
70 if (hasFill && ! hasSizes) {
71 out. push ({
72 pattern: metadata.id,
73 subtype: 'image-fill-no-sizes' ,
74 file: path,
75 line: lineOf (content, m.index),
76 evidence: tag. slice ( 0 , 200 ),
77 trafficIndependent: metadata.trafficIndependent,
78 });
79 }
80 // Inline data: URLs never round-trip through the optimizer.
81 const srcMatch = / \b src \s * = \s * ["'] ( [ ^ "'] + ) ["'] / . exec (tag);
82 if (srcMatch) {
83 const src = srcMatch[ 1 ];
84 if ( / \. svg( \? |$ )/ i . test (src) && ! src. startsWith ( 'data:' ) && ! / \b unoptimized \b / . test (tag)) {
85 out. push ({
86 pattern: metadata.id,
87 subtype: 'image-svg-no-unoptimized' ,
88 file: path,
89 line: lineOf (content, m.index),
90 evidence: tag. slice ( 0 , 200 ),
91 trafficIndependent: metadata.trafficIndependent,
92 });
93 }
94 }
95 }
96 }
97 }
98 return out;
99 }
100
101 import { lineOf } from '../util.mjs' ;
102
103 function isJsxLike ( path ) {
104 return / \. (tsx | jsx | html | svelte | astro | vue) $ / . test (path);
105 }
106 function isNextConfig ( path ) {
107 return /(?: ^| \/ )next \. config \. (js | mjs | ts | cjs) $ / . test (path);
108 }
109 function snippet ( text , idx ) {
110 const start = text. lastIndexOf ( ' \n ' , idx) + 1 ;
111 const end = text. indexOf ( ' \n ' , idx);
112 return text. slice (start, end === - 1 ? text. length : end). trim (). slice ( 0 , 160 );
113 }