# JavaScript without npm?
A dive into Rails 7's importmap approach Using JavaScript in Rails has a very long history. With Rails 7, the latest approach has been to move away from the tools from the JavaScript community like npm, yarn, and Webpack. Instead, Rails 7 introduces [importmap-rails](https://github.com/rails/importmap-rails) as a solution that “embraces the platform” and uses native JavaScript modules (also known as “ES Modules” or “ESM”). ## What’s Importmap-rails? Importmap-rails is touted as a gem that “let you import JavaScript modules directly from the browser” 1. The documentation claims that “this frees you from needing Webpack, Yarn, npm, or any other part of the JavaScript toolchain.” At first, I felt like those claims are a bit ambiguous and a bit of marketing hype. After exploring importmap-rails a bit more, I think I have another way of looking at it. importmap-rails is a replacement for npm for app builders. That is: take away npm’s authoring tools (like `npm publish`), and you might get a tool that has importmap-rails’s feature set. Have a look at a few things importmap does: - `importmap pin` adds a package (like `npm install`.) - `importmap.rb` is a file that list your packages (like `package.json`.) - `importmap audit` checks for security bulletins for the packages in importmap.rb (like `npm audit`.) - `impotrmap outdated` checks for outdated packages (like `npm outdated`). ## Importmap command Rails 7 apps provide a `bin/importmap` command, which is an analogue of what `npm` or `yarn` would be used for. ``` › bin/importmap Commands: importmap audit # Run a security audit importmap help [COMMAND] # Describe available commands or one specific command importmap json # Show the full importmap in json importmap outdated # Check for outdated packages importmap pin [*PACKAGES] # Pin new packages importmap unpin [*PACKAGES] # Unpin existing packages ``` ## Adding a package To add a package, use `bin/importmap pin `, much like you might do with `npm install` or `yarn add`. ``` › bin/importmap pin canvas-confetti Pinning "canvas-confetti" to https://ga.jspm.io/npm:canvas-confetti@1.5.1/dist/confetti.module.mjs ``` This adds a line to the `config/importmap.rb` to take note of that package as a dependency. Rather than put the files in `node_modules` like npm, it only takes note of a [jspm cdn](https://jspm.org/import-map-cdn) URL. ``` # Pin npm packages by running ./bin/importmap pin "application", preload: true pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true pin_all_from "app/javascript/controllers", under: "controllers" pin "canvas-confetti", to: "https://ga.jspm.io/npm:canvas-confetti@1.5.1/dist/confetti.module.mjs" ``` ## Importmap markup In development, Rails will render a ` ``` ## Aside: Importing JavaScript in browsers Being able to import JavaScript files from other JavaScript files has always been something that had many different approaches. Even Rails had it’s own—[sprockets](https://github.com/rails/sprockets-rails)—which continues to be supported to this day in Rails 7. Is wasn’t long until a new standard was made to be the native way for importing JavaScript files. This new standard way is called JavaScript modules. 3 In the wild, it might be more commonly known as “ES modules” or “ESM” after its name prior to standardisation, but it’s JavaScript modules all the same. Browsers have supported JavaScript modules since 2017. This `import` statement will work in any modern browser today, as long as it’s placed in a ` ``` ![[2022-08-30 canvas confetti codepen.mov]] ## About importmaps Being able to import straight from URL’s is great, but it can easily get repetitive. To work around this, an “import map” can be used to alias URL’s into easily-readable names. The gem importmap-rails got its name because generating these importmap blocks is sole bread-and-butter. It’s the mechanism that allows developers use 3rd-party JavaScripts commonly found in npm in the browser, all without having to modify scripts. ``` ``` ## Browser support for import maps In 2022, import maps are only supported by the latest Chrome, Edge, and Opera browsers. Even the Webkit-based Safari doesn’t support it natively yet, and Firefox doesn’t either. It’s still considered an unofficial standard. ![[Pasted image 20220830105604.png]] Source: https://caniuse.com/import-maps ## ES module shim Rails will also load a package called [es-module-shims](https://github.com/guybedford/es-module-shims). This seems to be behaviour [built into Rails’s importmap-rails](https://github.com/rails/importmap-rails/search?q=es-module-shims). ```html ``` It seems es-module-shims adds a bit of runtime cost. Even for a modern browser like Firefox 103, the console tells me there’s some 2ms being spent in compiling the asm.js code. This is despite es-module-shims claiming that *“users with import maps entirely bypass the shim code entirely”* 2 — that’s because importmaps aren’t supported in Firefox (yet). ![[Pasted image 20220830103238.png]] ## Importing application.js Finally, Rails will add an import for `application.js` by ways of a ` ``` ## Module preload To speed things along, Rails will also generate `` tags. Typically, the browser will have to download application.js to find its dependencies, leading to a “waterfall” style download where files are downloaded in stages. Instead, these modulepreload tags will signal browsers to [prefetch](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/modulepreload) scripts that are dependenies of application.js, so they can be downloaded right away. ```html ``` ## Is un-bundling faster? I tried looking for benchmarks on whether this approach of loading many small files (“un-bundling”), is better than one big file (“bundling”). I wasn’t able to find any, but anecdotally, there are reports that JavaScript modules and HTTP2 aren’t faster than bundlers at all. In fact, they may even be slower, possibly due to the way compression works. - https://twitter.com/devongovett/status/1257342740691550208 On the JavaScript community, there’s been progress on being less reliant on bundling. [Vite](https://vitejs.dev/) is one of these “un-bundlers” that take advantage of JavaScript module support in the browser. However, it still performs bundling in production because it’s a very efficient way to serve files to browsers. - https://twitter.com/youyuxi/status/1257367902371618818 ## Limitations of jspm With importmap-rails, third-party packages will be loaded from a public CDN, jspm. This might be a concern with private apps. If jspm is down, sites that use it will be affected as well. ## No React, no TypeScript With importmap-rails, there is no build step that proccesses JavaScript files. That means it’s not possible to use things like TypeScript, React, Vue, Svelte, Solid, Angular, or any of the common JavaScript tools today used to build frontends. Rather than rely on tools like React, the Rails community suggests to use [Stimulus](https://stimulus.hotwired.dev/) instead. Stimulus also comes default with Rails 7. ## More limitations No building. No tree-shaking. No CSS modules. No automated upgrade tools like Dependabot (yet). No automated security audits like Snyk (yet). ## My thoughts Pros: - Simple, no bundlers to configure - Faster deployments, no “npm install” or bundling Cons: - Hard dependency on a public CDN - Downtime in public CDN’s will affect your site - Runtime cost for Firefox and Safari - No support for TypeScript - No support for React (and other tools needing a build step) - No automated upgrade tools like Dependabot - No automated security audits like Snyk ## Links 1. https://github.com/rails/importmap-rails#importmap-for-rails 2. https://github.com/guybedford/es-module-shims#es-module-shims 3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules 4. https://web.dev/performance-http2/