[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-import-maps-bare-specifiers-native-modules":44,"search-suggestions":239,"related-import-maps-bare-specifiers-native-modules":276,"code:plain:true:16y220h":404,"code:js:true:p4c4qp":405,"code:html:true:1p7wj8j":406,"code:json:true:l6cng3":407,"comments-01a013a2-ff42-738e-b6d0-cda8f338a976":408},[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,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":222,"seo":223,"translationGroupId":226,"thread":227,"assessments":229,"translations":235,"quiz":237},"01a013a2-ff42-738e-b6d0-cda8f338a976","import-maps-bare-specifiers-native-modules","One version bump, 40 files to edit. Import maps fix that.",null,"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",5,"2026-08-23T06:48:56.911Z",44,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"frontend","Frontend",{"slug":73,"name":74,"color":48},"browser","Browser",{"assessments":76},1,{"slug":46,"title":78},"Import maps — interactive resolver",{"blocks":80,"version":76},[81,85,88,93,96,102,105,108,113,116,119,122,127,130,133,136,139,144,147,150,153,156,159,162,165,168,171,174,177,180,183,186,189,192,195,198,201,204,207,210,213],{"id":82,"html":83,"type":84},"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":86,"html":87,"type":84},"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":89,"html":90,"text":91,"type":92,"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":94,"html":95,"type":84},"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":97,"code":98,"type":99,"language":100,"highlight":101},"b5","Uncaught TypeError: Failed to resolve module specifier \"lodash\".\nRelative references must start with either \"\u002F\", \".\u002F\", or \"..\u002F\".","code","plain",[],{"id":103,"html":104,"type":84},"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":106,"html":107,"type":84},"b7","\u003Cp>So teams without a bundler reach for the next-easiest thing: paste the full CDN URL directly.\u003C\u002Fp>",{"id":109,"code":110,"type":99,"language":111,"highlight":112},"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":114,"html":115,"type":84},"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":117,"html":118,"text":118,"type":92,"level":31},"b10","The browser has had the real fix since 2023",{"id":120,"html":121,"type":84},"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":123,"code":124,"type":99,"language":125,"highlight":126},"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>","html",[],{"id":128,"html":129,"type":84},"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":131,"html":132,"type":84},"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":134,"html":135,"text":135,"type":92,"level":31},"b15","When two parts of your app need two versions",{"id":137,"html":138,"type":84},"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":140,"code":141,"type":99,"language":142,"highlight":143},"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":145,"html":146,"type":84},"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":148,"html":149,"text":149,"type":92,"level":31},"b19","Three things that will bite you",{"id":151,"html":152,"type":84},"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":154,"html":155,"type":84},"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":157,"html":158,"type":84},"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":160,"html":161,"text":161,"type":92,"level":31},"b23","Where this actually earns its keep",{"id":163,"html":164,"type":84},"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":166,"html":167,"type":84},"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":169,"html":170,"type":84},"b26","\u003C!-- playground:start -->",{"id":172,"html":173,"text":173,"type":92,"level":31},"b27","🎮 Try it yourself",{"id":175,"html":176,"type":84},"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":178,"html":179,"type":84},"b29","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":181,"html":182,"type":84},"b30","\u003C!-- playground:end -->",{"id":184,"html":185,"type":84},"b31","\u003C!-- quiz:start -->",{"id":187,"html":188,"text":188,"type":92,"level":31},"b32","🧠 Test yourself",{"id":190,"html":191,"type":84},"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":193,"html":194,"type":84},"b34","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":196,"html":197,"type":84},"b35","\u003C!-- quiz:end -->",{"id":199,"html":200,"text":200,"type":92,"level":31},"b36","The takeaway",{"id":202,"html":203,"type":84},"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":205,"html":206,"type":84},"b38","\u003Cp>Have you shipped a no-build app that outgrew hardcoded CDN imports — and if so, what finally broke first?\u003C\u002Fp>",{"id":208,"type":209},"b39","divider",{"id":211,"html":212,"type":84},"b40","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":214,"type":215,"items":216,"ordered":18},"b41","list",[217,218,219,220,221],"⭐ \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":47,"canonical":224,"description":225},"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":228,"locked":18},"01a013a2-ff71-708f-9d83-6f2792cd4345",[230],{"id":231,"slug":46,"title":232,"_count":233},"01a013a2-ff91-722c-bd4b-49d5c393859c","Import maps: resolving bare specifiers natively",{"questions":234},9,[236],{"locale":13,"slug":46},{"id":231,"slug":46,"title":232,"_count":238,"questionCount":234},{"questions":234},[240,242,244,246,250,254,257,261,264,267,270,273],{"slug":67,"name":68,"articles":241},62,{"slug":70,"name":71,"articles":243},56,{"slug":61,"name":65,"articles":245},53,{"slug":247,"name":248,"articles":249},"css","Css",23,{"slug":251,"name":252,"articles":253},"typescript","Typescript",10,{"slug":255,"name":256,"articles":234},"performance","Performance",{"slug":258,"name":259,"articles":260},"grammar","Grammar",6,{"slug":262,"name":263,"articles":51},"react","React",{"slug":125,"name":265,"articles":266},"Html",4,{"slug":268,"name":269,"articles":266},"tutorial","Tutorial",{"slug":271,"name":272,"articles":266},"node","Node",{"slug":274,"name":275,"articles":43},"ai","Ai",{"items":277,"meta":401},[278,290,308,326,345,365,383],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":279,"likeCount":19,"commentCount":19,"author":280,"vertical":281,"topic":282,"tags":283,"_count":288,"playground":289,"hasQuiz":17,"hasPlayground":17},45,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[284,285,286,287],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":291,"slug":292,"title":293,"subtitle":48,"excerpt":294,"coverUrl":295,"locale":13,"readingMinutes":266,"publishedAt":296,"viewCount":297,"likeCount":19,"commentCount":19,"author":298,"vertical":299,"topic":300,"tags":301,"_count":306,"playground":307,"hasQuiz":17,"hasPlayground":17},"019ff19b-fbc7-7069-b3ba-28ffd8fbf4d3","navigator-sendbeacon-reliable-analytics","Your fetch in beforeunload is silently killed. navigator.sendBeacon guarantees delivery.","When the page unloads, the browser may cancel any in-flight fetch or XHR before it completes. navigator.sendBeacon sends a POST the browser commits to delivering, even after the document is gone.","\u002Fmedia\u002Fcovers\u002Fnavigator-sendbeacon-reliable-analytics.png","2026-08-22T06:47:21.180Z",86,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[302,303,304,305],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":255,"name":256,"color":48},{"assessments":76},{"slug":292},{"id":309,"slug":310,"title":311,"subtitle":48,"excerpt":312,"coverUrl":313,"locale":13,"readingMinutes":260,"publishedAt":314,"viewCount":315,"likeCount":19,"commentCount":19,"author":316,"vertical":317,"topic":318,"tags":319,"_count":324,"playground":325,"hasQuiz":17,"hasPlayground":17},"01a00952-9d58-7601-89ac-42fc7bf00fc9","urlpattern-native-route-matching","Stop Writing Regex to Match URLs — The Browser Already Can","A service worker's route regex was missing one anchor and started caching live edit forms as if they were read-only pages. URLPattern, native since 2021, doesn't let you make that mistake.","\u002Fmedia\u002Fcovers\u002Furlpattern-native-route-matching.png","2026-08-20T06:53:07.359Z",163,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[320,321,322,323],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":73,"name":74,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":310},{"id":327,"slug":328,"title":329,"subtitle":48,"excerpt":330,"coverUrl":331,"locale":13,"readingMinutes":266,"publishedAt":332,"viewCount":333,"likeCount":19,"commentCount":19,"author":334,"vertical":335,"topic":336,"tags":338,"_count":343,"playground":344,"hasQuiz":17,"hasPlayground":17},"019ffa10-eeed-707d-9b50-7d83664c7a9a","css-trigonometric-functions-radial-layout","Stop Writing Math.cos() in a Resize Listener","CSS learned trigonometry. Here's the radial menu that used to need a resize listener — and doesn't anymore.","\u002Fmedia\u002Fcovers\u002Fcss-trigonometric-functions-radial-layout.png","2026-08-17T07:03:57.776Z",394,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":247,"name":337},"CSS",[339,340,341,342],{"slug":247,"name":248,"color":48},{"slug":67,"name":68,"color":48},{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":328},{"id":346,"slug":347,"title":348,"subtitle":48,"excerpt":349,"coverUrl":350,"locale":13,"readingMinutes":51,"publishedAt":351,"viewCount":352,"likeCount":19,"commentCount":19,"author":353,"vertical":354,"topic":355,"tags":356,"_count":363,"playground":364,"hasQuiz":17,"hasPlayground":17},"019ff19b-f9ca-772e-8f09-cd3d3c640d2b","web-locks-api-tab-coordination","Five tabs open, one refresh token — the race nobody noticed","Users with multiple tabs open triggered a silent race: every tab refreshed the same expiring auth token at once, and the server started invalidating its own sessions. navigator.locks fixes it with a real mutex, no polling or localStorage hacks required.","\u002Fmedia\u002Fcovers\u002Fweb-locks-api-tab-coordination.png","2026-08-15T06:44:54.917Z",561,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[357,358,359,360],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":361,"name":362,"color":48},"async","Async",{"assessments":76},{"slug":347},{"id":366,"slug":367,"title":368,"subtitle":48,"excerpt":369,"coverUrl":370,"locale":13,"readingMinutes":43,"publishedAt":371,"viewCount":372,"likeCount":19,"commentCount":19,"author":373,"vertical":374,"topic":375,"tags":376,"_count":381,"playground":382,"hasQuiz":17,"hasPlayground":17},"019fe660-8bc3-7444-8c96-86953902f5e0","findlast-search-from-end","You copy and reverse the array to find the last match. `findLast()` searches from the end directly.","The usual workaround — `[...arr].reverse().find()` — allocates a full copy just to walk it backward. ES2023 added `findLast()` and `findLastIndex()` to search from the end without touching the original.","\u002Fmedia\u002Fcovers\u002Ffindlast-search-from-end.png","2026-08-14T07:38:34.486Z",378,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[377,378,379,380],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":251,"name":252,"color":48},{"assessments":76},{"slug":367},{"id":384,"slug":385,"title":386,"subtitle":48,"excerpt":387,"coverUrl":388,"locale":13,"readingMinutes":266,"publishedAt":389,"viewCount":390,"likeCount":19,"commentCount":19,"author":391,"vertical":392,"topic":393,"tags":394,"_count":399,"playground":400,"hasQuiz":17,"hasPlayground":17},"019fe661-0450-720a-ad6e-5eb995429cd7","sendbeacon-reliable-unload-data","Your `fetch()` in `beforeunload` is being silently dropped. Use `navigator.sendBeacon()`.","Browsers cancel in-flight network requests when a page unloads. The common fix — synchronous XHR — is deprecated. `navigator.sendBeacon()` is the correct, fire-and-forget API designed exactly for this case.","\u002Fmedia\u002Fcovers\u002Fsendbeacon-reliable-unload-data.png","2026-08-13T07:40:48.196Z",406,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[395,396,397,398],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":255,"name":256,"color":48},{"assessments":76},{"slug":385},{"page":76,"perPage":402,"total":245,"totalPages":403},7,8,"\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">Uncaught TypeError: Failed to resolve module specifier \u003Cspan class=\"shj-str\">\"lodash\"\u003C\u002Fspan>.\nRelative references must start with either \u003Cspan class=\"shj-str\">\"\u002F\"\u003C\u002Fspan>, \u003Cspan class=\"shj-str\">\".\u002F\"\u003C\u002Fspan>, or \u003Cspan class=\"shj-str\">\"..\u002F\"\u003C\u002Fspan>.\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> debounce \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002Fdebounce\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> throttle \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002Fthrottle\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> cloneDeep \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002FcloneDeep\"\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-html shj-multiline\" data-lang=\"html\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-class\">type\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"importmap\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \"imports\"\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \"lodash-es\u002F\"\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.21\u002F\"\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-class\">type\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"module\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> debounce \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"lodash-es\u002Fdebounce\"\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> throttle \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"lodash-es\u002Fthrottle\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-json shj-multiline\" data-lang=\"json\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">\"imports\"\u003C\u002Fspan>: \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\"lodash-es\u002F\"\u003C\u002Fspan>: \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.22\u002F\"\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>,\n  \u003Cspan class=\"shj-var\">\"scopes\"\u003C\u002Fspan>: \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\"\u002Fadmin\u002F\"\u003C\u002Fspan>: \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\"lodash-es\u002F\"\u003C\u002Fspan>: \u003Cspan class=\"shj-str\">\"https:\u002F\u002Fesm.sh\u002Flodash-es@4.17.19\u002F\"\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":409},[]]