[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-import-maps-bare-specifiers-native-modules":44,"search-suggestions":60,"quiz-article-import-maps-bare-specifiers-native-modules":105},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a013a2-ff91-722c-bd4b-49d5c393859c","import-maps-bare-specifiers-native-modules","PRACTICE_QUIZ","Import maps: resolving bare specifiers natively","Nine questions on how browsers resolve bare module specifiers, what an import map actually changes, and the gotchas around ordering, scopes, and cross-browser support.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},9,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,84,88,92,96,99,102],{"slug":62,"name":63,"articles":64},"webdev","Webdev",62,{"slug":66,"name":67,"articles":68},"frontend","Frontend",56,{"slug":70,"name":71,"articles":72},"javascript","Javascript",53,{"slug":74,"name":75,"articles":76},"css","Css",23,{"slug":78,"name":79,"articles":80},"typescript","Typescript",10,{"slug":82,"name":83,"articles":51},"performance","Performance",{"slug":85,"name":86,"articles":87},"grammar","Grammar",6,{"slug":89,"name":90,"articles":91},"react","React",5,{"slug":93,"name":94,"articles":95},"html","Html",4,{"slug":97,"name":98,"articles":95},"tutorial","Tutorial",{"slug":100,"name":101,"articles":95},"node","Node",{"slug":103,"name":104,"articles":43},"ai","Ai",{"id":106,"slug":46,"title":107,"subtitle":52,"excerpt":108,"coverUrl":109,"locale":13,"readingMinutes":91,"publishedAt":110,"viewCount":111,"likeCount":19,"commentCount":19,"author":112,"vertical":117,"topic":118,"tags":120,"_count":127,"playground":129,"body":131,"bodyMd":273,"seo":274,"translationGroupId":277,"thread":278,"assessments":280,"translations":283,"quiz":285},"01a013a2-ff42-738e-b6d0-cda8f338a976","One version bump, 40 files to edit. Import maps fix that.","Bare specifiers like import _ from 'lodash' don't work in a browser without a bundler — unless you tell the browser how to resolve them yourself, in one JSON block instead of forty hardcoded URLs.","\u002Fmedia\u002Fcovers\u002Fimport-maps-bare-specifiers-native-modules.png","2026-08-23T06:48:56.911Z",48,{"id":113,"name":114,"username":115,"avatarUrl":52,"headline":116},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":119},"JavaScript",[121,122,123,124],{"slug":70,"name":71,"color":52},{"slug":62,"name":63,"color":52},{"slug":66,"name":67,"color":52},{"slug":125,"name":126,"color":52},"browser","Browser",{"assessments":128},1,{"slug":46,"title":130},"Import maps — interactive resolver",{"blocks":132,"version":128},[133,137,140,145,148,154,157,160,165,168,171,174,178,181,184,187,190,195,198,201,204,207,210,213,216,219,222,225,228,231,234,237,240,243,246,249,252,255,258,261,264],{"id":134,"html":135,"type":136},"b1","\u003Cp>\u003Ccode>grep -rl &quot;esm.sh\u002Flodash-es@4.17.21&quot; src\u002F\u003C\u002Fcode> came back with forty-one files. Forty-one places where the same CDN URL, pinned to the same version, was typed out by hand at the top of a module. The security advisory said bump to 4.17.22. A \u003Ccode>sed\u003C\u002Fcode> one-liner across the repo felt fine until it silently skipped the two files that imported a named subpath with a slightly different query string.\u003C\u002Fp>","paragraph",{"id":138,"html":139,"type":136},"b2","\u003Cp>That&#39;s the moment you find out your &quot;no build step&quot; app has a build-step problem anyway — you just moved it into your find-and-replace history.\u003C\u002Fp>",{"id":141,"html":142,"text":143,"type":144,"level":31},"b3","Why you can&#39;t just write \u003Ccode>import _ from &quot;lodash&quot;\u003C\u002Fcode>","Why you can't just write import  from \"lodash\"","heading",{"id":146,"html":147,"type":136},"b4","\u003Cp>Node resolves that specifier by walking \u003Ccode>node_modules\u003C\u002Fcode>. A browser has no \u003Ccode>node_modules\u003C\u002Fcode> to walk. Try it in a plain \u003Ccode>&lt;script type=&quot;module&quot;&gt;\u003C\u002Fcode> and you get this:\u003C\u002Fp>",{"id":149,"code":150,"type":151,"language":152,"highlight":153},"b5","Uncaught TypeError: Failed to resolve module specifier \"lodash\".\nRelative references must start with either \"\u002F\", \".\u002F\", or \"..\u002F\".","code","plain",[],{"id":155,"html":156,"type":136},"b6","\u003Cp>The ES module spec only lets a browser resolve three kinds of specifiers on its own: a full URL, or a path that starts with \u003Ccode>\u002F\u003C\u002Fcode>, \u003Ccode>.\u002F\u003C\u002Fcode>, or \u003Ccode>..\u002F\u003C\u002Fcode>. \u003Ccode>&quot;lodash&quot;\u003C\u002Fcode> is none of those — it&#39;s a \u003Cem>bare specifier\u003C\u002Fem>, and bare specifiers only mean something if a bundler rewrites them at build time, or a package manager&#39;s resolution algorithm (the kind Node has, and browsers don&#39;t) fills in the blank.\u003C\u002Fp>",{"id":158,"html":159,"type":136},"b7","\u003Cp>So teams without a bundler reach for the next-easiest thing: paste the full CDN URL directly.\u003C\u002Fp>",{"id":161,"code":162,"type":151,"language":163,"highlight":164},"b8","import debounce from \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002Fdebounce\";\nimport throttle from \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002Fthrottle\";\nimport cloneDeep from \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002FcloneDeep\";","js",[],{"id":166,"html":167,"type":136},"b9","\u003Cp>It works. It also means the version number is now a string literal duplicated in every file that imports anything from that package — which is exactly the forty-one-files problem above, just discovered slightly later.\u003C\u002Fp>",{"id":169,"html":170,"text":170,"type":144,"level":31},"b10","The browser has had the real fix since 2023",{"id":172,"html":173,"type":136},"b11","\u003Cp>An \u003Cstrong>import map\u003C\u002Fstrong> is a small JSON block that tells the browser: &quot;when you see this bare specifier, resolve it to this URL.&quot; One declaration, and every \u003Ccode>import\u003C\u002Fcode> statement in the page — in every module, in every file — goes through it.\u003C\u002Fp>",{"id":175,"code":176,"type":151,"language":93,"highlight":177},"b12","\u003Cscript type=\"importmap\">\n{\n  \"imports\": {\n    \"lodash-es\u002F\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002F\"\n  }\n}\n\u003C\u002Fscript>\n\u003Cscript type=\"module\">\n  import debounce from \"lodash-es\u002Fdebounce\";\n  import throttle from \"lodash-es\u002Fthrottle\";\n\u003C\u002Fscript>",[],{"id":179,"html":180,"type":136},"b13","\u003Cp>Now every file in your app writes \u003Ccode>import debounce from &quot;lodash-es\u002Fdebounce&quot;\u003C\u002Fcode> — a specifier that looks exactly like the one that would fail with no import map at all. The version lives in exactly one place. Bumping it is a one-line diff, not a repo-wide search.\u003C\u002Fp>",{"id":182,"html":183,"type":136},"b14","\u003Cp>Notice the trailing slash on both the key and the value — that&#39;s not decoration. A key ending in \u003Ccode>\u002F\u003C\u002Fcode> is a \u003Cem>prefix\u003C\u002Fem> mapping: anything that starts with \u003Ccode>lodash-es\u002F\u003C\u002Fcode> gets that prefix swapped for the mapped URL, and the rest of the specifier is appended. The value must end in \u003Ccode>\u002F\u003C\u002Fcode> too, or the browser refuses to register the mapping at all. Try \u003Ccode>&quot;lodash-es&quot;: &quot;https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21&quot;\u003C\u002Fcode> — no trailing slash — and it&#39;s only a mapping for the \u003Cem>exact\u003C\u002Fem> specifier \u003Ccode>&quot;lodash-es&quot;\u003C\u002Fcode>, not for \u003Ccode>&quot;lodash-es\u002Fdebounce&quot;\u003C\u002Fcode>.\u003C\u002Fp>",{"id":185,"html":186,"text":186,"type":144,"level":31},"b15","When two parts of your app need two versions",{"id":188,"html":189,"type":136},"b16","\u003Cp>Say your main app is on \u003Ccode>lodash-es@4.17.22\u003C\u002Fcode>, but a legacy \u003Ccode>\u002Fadmin\u003C\u002Fcode> section is pinned to \u003Ccode>4.17.19\u003C\u002Fcode> until someone gets around to testing it against the new one. That&#39;s what \u003Ccode>scopes\u003C\u002Fcode> is for — a fallback map keyed by the \u003Cem>path of the importing module\u003C\u002Fem>, checked before the top-level \u003Ccode>imports\u003C\u002Fcode>:\u003C\u002Fp>",{"id":191,"code":192,"type":151,"language":193,"highlight":194},"b17","{\n  \"imports\": {\n    \"lodash-es\u002F\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.22\u002F\"\n  },\n  \"scopes\": {\n    \"\u002Fadmin\u002F\": {\n      \"lodash-es\u002F\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.19\u002F\"\n    }\n  }\n}","json",[],{"id":196,"html":197,"type":136},"b18","\u003Cp>Every module under \u003Ccode>\u002Fadmin\u002F\u003C\u002Fcode> resolves \u003Ccode>lodash-es\u002F*\u003C\u002Fcode> to the pinned version; everything else gets the current one. If a specifier matches more than one scope path, the browser picks the longest — the most specific one wins, the same way specificity works everywhere else on the platform.\u003C\u002Fp>",{"id":199,"html":200,"text":200,"type":144,"level":31},"b19","Three things that will bite you",{"id":202,"html":203,"type":136},"b20","\u003Cp>\u003Cstrong>Order matters — a lot.\u003C\u002Fstrong> The import map has to appear before any module script that relies on it. Put it late in \u003Ccode>&lt;head&gt;\u003C\u002Fcode>, after a \u003Ccode>&lt;script type=&quot;module&quot;&gt;\u003C\u002Fcode> has already started resolving imports, and you&#39;ve written a JSON block that does nothing.\u003C\u002Fp>",{"id":205,"html":206,"type":136},"b21","\u003Cp>\u003Cstrong>You get one per page, for now.\u003C\u002Fstrong> Declare two \u003Ccode>&lt;script type=&quot;importmap&quot;&gt;\u003C\u002Fcode> tags and current browsers will register the first and ignore the second with a console warning — the spec&#39;s story on \u003Cem>merging\u003C\u002Fem> multiple import maps is still catching up across engines. Plan your mappings as one block, not several you&#39;ll combine later.\u003C\u002Fp>",{"id":208,"html":209,"type":136},"b22","\u003Cp>\u003Cstrong>\u003Ccode>src\u003C\u002Fcode> is a Chrome-only shortcut today.\u003C\u002Fstrong> You can point an import map at an external JSON file with \u003Ccode>&lt;script type=&quot;importmap&quot; src=&quot;\u002Fimport-map.json&quot;&gt;\u003C\u002Fcode> — but as of now, Firefox and Safari only accept the inline form. If you want the map to work everywhere, write the JSON directly inside the \u003Ccode>&lt;script&gt;\u003C\u002Fcode> tag, not in a file you link to.\u003C\u002Fp>",{"id":211,"html":212,"text":212,"type":144,"level":31},"b23","Where this actually earns its keep",{"id":214,"html":215,"type":136},"b24","\u003Cp>Import maps aren&#39;t a bundler replacement — you still don&#39;t get tree-shaking, minification, or a single merged file out of one. What they replace is the specific pain of coordinating dependency URLs by hand: no-build multi-page sites, quick prototypes that don&#39;t want a \u003Ccode>node_modules\u003C\u002Fcode> yet, and micro-frontends where two independently-deployed teams need to agree on one shared React instance without agreeing on a build tool. If your app already ships through Vite or webpack, you get this exact benefit — one place to change a version — from the bundler config instead, and there&#39;s no reason to add a second mechanism doing the same job.\u003C\u002Fp>",{"id":217,"html":218,"type":136},"b25","\u003Cp>But if you&#39;ve ever pasted a CDN URL into a file and thought &quot;I&#39;ll deal with the version number later,&quot; this is the later. Open your dev tools console right now and try \u003Ccode>import(&quot;lodash&quot;)\u003C\u002Fcode> on any page without an import map — read the error it gives you, then imagine one JSON block making it resolve instead.\u003C\u002Fp>",{"id":220,"html":221,"type":136},"b26","\u003C!-- playground:start -->",{"id":223,"html":224,"text":224,"type":144,"level":31},"b27","🎮 Try it yourself",{"id":226,"html":227,"type":136},"b28","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fimport-maps-bare-specifiers-native-modules\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":229,"html":230,"type":136},"b29","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":232,"html":233,"type":136},"b30","\u003C!-- playground:end -->",{"id":235,"html":236,"type":136},"b31","\u003C!-- quiz:start -->",{"id":238,"html":239,"text":239,"type":144,"level":31},"b32","🧠 Test yourself",{"id":241,"html":242,"type":136},"b33","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fimport-maps-bare-specifiers-native-modules\u002Fquiz\">Take the 9-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":244,"html":245,"type":136},"b34","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":247,"html":248,"type":136},"b35","\u003C!-- quiz:end -->",{"id":250,"html":251,"text":251,"type":144,"level":31},"b36","The takeaway",{"id":253,"html":254,"type":136},"b37","\u003Cp>A bare specifier like \u003Ccode>&quot;lodash&quot;\u003C\u002Fcode> means nothing to a browser on its own — it needs a bundler to rewrite it, or an import map to tell it where to look. The map is one JSON block: \u003Ccode>imports\u003C\u002Fcode> for the default resolution, \u003Ccode>scopes\u003C\u002Fcode> for path-specific overrides, trailing slashes for prefix matching, and it has to load before the first module that needs it. Next time you catch yourself duplicating a CDN URL across files, that&#39;s the smell import maps were built to fix.\u003C\u002Fp>",{"id":256,"html":257,"type":136},"b38","\u003Cp>Have you shipped a no-build app that outgrew hardcoded CDN imports — and if so, what finally broke first?\u003C\u002Fp>",{"id":259,"type":260},"b39","divider",{"id":262,"html":263,"type":136},"b40","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":265,"type":266,"items":267,"ordered":18},"b41","list",[268,269,270,271,272],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","`grep -rl \"esm.sh\u002Flodash-es@4.17.21\" src\u002F` came back with forty-one files. Forty-one places where the same CDN URL, pinned to the same version, was typed out by hand at the top of a module. The security advisory said bump to 4.17.22. A `sed` one-liner across the repo felt fine until it silently skipped the two files that imported a named subpath with a slightly different query string.\n\nThat's the moment you find out your \"no build step\" app has a build-step problem anyway — you just moved it into your find-and-replace history.\n\n## Why you can't just write `import _ from \"lodash\"`\n\nNode resolves that specifier by walking `node_modules`. A browser has no `node_modules` to walk. Try it in a plain `\u003Cscript type=\"module\">` and you get this:\n\n```\nUncaught TypeError: Failed to resolve module specifier \"lodash\".\nRelative references must start with either \"\u002F\", \".\u002F\", or \"..\u002F\".\n```\n\nThe ES module spec only lets a browser resolve three kinds of specifiers on its own: a full URL, or a path that starts with `\u002F`, `.\u002F`, or `..\u002F`. `\"lodash\"` is none of those — it's a *bare specifier*, and bare specifiers only mean something if a bundler rewrites them at build time, or a package manager's resolution algorithm (the kind Node has, and browsers don't) fills in the blank.\n\nSo teams without a bundler reach for the next-easiest thing: paste the full CDN URL directly.\n\n```js\nimport debounce from \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002Fdebounce\";\nimport throttle from \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002Fthrottle\";\nimport cloneDeep from \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002FcloneDeep\";\n```\n\nIt works. It also means the version number is now a string literal duplicated in every file that imports anything from that package — which is exactly the forty-one-files problem above, just discovered slightly later.\n\n## The browser has had the real fix since 2023\n\nAn **import map** is a small JSON block that tells the browser: \"when you see this bare specifier, resolve it to this URL.\" One declaration, and every `import` statement in the page — in every module, in every file — goes through it.\n\n```html\n\u003Cscript type=\"importmap\">\n{\n  \"imports\": {\n    \"lodash-es\u002F\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002F\"\n  }\n}\n\u003C\u002Fscript>\n\u003Cscript type=\"module\">\n  import debounce from \"lodash-es\u002Fdebounce\";\n  import throttle from \"lodash-es\u002Fthrottle\";\n\u003C\u002Fscript>\n```\n\nNow every file in your app writes `import debounce from \"lodash-es\u002Fdebounce\"` — a specifier that looks exactly like the one that would fail with no import map at all. The version lives in exactly one place. Bumping it is a one-line diff, not a repo-wide search.\n\nNotice the trailing slash on both the key and the value — that's not decoration. A key ending in `\u002F` is a *prefix* mapping: anything that starts with `lodash-es\u002F` gets that prefix swapped for the mapped URL, and the rest of the specifier is appended. The value must end in `\u002F` too, or the browser refuses to register the mapping at all. Try `\"lodash-es\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\"` — no trailing slash — and it's only a mapping for the *exact* specifier `\"lodash-es\"`, not for `\"lodash-es\u002Fdebounce\"`.\n\n## When two parts of your app need two versions\n\nSay your main app is on `lodash-es@4.17.22`, but a legacy `\u002Fadmin` section is pinned to `4.17.19` until someone gets around to testing it against the new one. That's what `scopes` is for — a fallback map keyed by the *path of the importing module*, checked before the top-level `imports`:\n\n```json\n{\n  \"imports\": {\n    \"lodash-es\u002F\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.22\u002F\"\n  },\n  \"scopes\": {\n    \"\u002Fadmin\u002F\": {\n      \"lodash-es\u002F\": \"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.19\u002F\"\n    }\n  }\n}\n```\n\nEvery module under `\u002Fadmin\u002F` resolves `lodash-es\u002F*` to the pinned version; everything else gets the current one. If a specifier matches more than one scope path, the browser picks the longest — the most specific one wins, the same way specificity works everywhere else on the platform.\n\n## Three things that will bite you\n\n**Order matters — a lot.** The import map has to appear before any module script that relies on it. Put it late in `\u003Chead>`, after a `\u003Cscript type=\"module\">` has already started resolving imports, and you've written a JSON block that does nothing.\n\n**You get one per page, for now.** Declare two `\u003Cscript type=\"importmap\">` tags and current browsers will register the first and ignore the second with a console warning — the spec's story on *merging* multiple import maps is still catching up across engines. Plan your mappings as one block, not several you'll combine later.\n\n**`src` is a Chrome-only shortcut today.** You can point an import map at an external JSON file with `\u003Cscript type=\"importmap\" src=\"\u002Fimport-map.json\">` — but as of now, Firefox and Safari only accept the inline form. If you want the map to work everywhere, write the JSON directly inside the `\u003Cscript>` tag, not in a file you link to.\n\n## Where this actually earns its keep\n\nImport maps aren't a bundler replacement — you still don't get tree-shaking, minification, or a single merged file out of one. What they replace is the specific pain of coordinating dependency URLs by hand: no-build multi-page sites, quick prototypes that don't want a `node_modules` yet, and micro-frontends where two independently-deployed teams need to agree on one shared React instance without agreeing on a build tool. If your app already ships through Vite or webpack, you get this exact benefit — one place to change a version — from the bundler config instead, and there's no reason to add a second mechanism doing the same job.\n\nBut if you've ever pasted a CDN URL into a file and thought \"I'll deal with the version number later,\" this is the later. Open your dev tools console right now and try `import(\"lodash\")` on any page without an import map — read the error it gives you, then imagine one JSON block making it resolve instead.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fimport-maps-bare-specifiers-native-modules\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 9-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fimport-maps-bare-specifiers-native-modules\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## The takeaway\n\nA bare specifier like `\"lodash\"` means nothing to a browser on its own — it needs a bundler to rewrite it, or an import map to tell it where to look. The map is one JSON block: `imports` for the default resolution, `scopes` for path-specific overrides, trailing slashes for prefix matching, and it has to load before the first module that needs it. Next time you catch yourself duplicating a CDN URL across files, that's the smell import maps were built to fix.\n\nHave you shipped a no-build app that outgrew hardcoded CDN imports — and if so, what finally broke first?\n\n---\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":107,"canonical":275,"description":276},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fimport-maps-bare-specifiers-native-modules","Bare specifiers like import _ from 'lodash' don't work in a browser without a bundler — unless you tell the browser how to resolve them yourself, in one JSON block instead of forty","01a013a2-ff42-738e-b6d0-d2ddbad528ef",{"id":279,"locked":18},"01a013a2-ff71-708f-9d83-6f2792cd4345",[281],{"id":45,"slug":46,"title":48,"_count":282},{"questions":51},[284],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":286,"questionCount":51},{"questions":51}]