Asked 1 month ago by OrbitalEngineer736
How can I optimize importing only needed parts of date-fns in Rails 8 using Importmap?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by OrbitalEngineer736
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
In my Rails 8 application, I’m using Importmap and need only specific functions (like formatDistanceToNow) along with a couple of locales from the date-fns library. Instead of importing the entire library, I downloaded all the date-fns files and placed them in the folder app/javascript/vendor/date-fns.
To configure, I updated my import map in config/importmap.rb as follows:
RUBY# config/importmap.rb // ... pin 'date-fns', to: 'vendor/date-fns/formatDistanceToNow.js' pin 'date-fns/locale/it', to: 'vendor/date-fns/locale/it.js' pin 'date-fns/locale/en-US', to: 'vendor/date-fns/locale/en-US.js'
Then, in my Stimulus controller, I imported the modules:
JAVASCRIPT# app/javascript/controllers/time_ago_controller.js import { Controller } from "@hotwired/stimulus" import { formatDistanceToNow } from "date-fns"; import { it } from "date-fns/locale/it"; import { enUS } from "date-fns/locale/en-US"; // Connects to data-controller="time-ago" export default class extends Controller { // ... }
And in my view I attached the controller:
HTML# show.html.erb <div data-controller="time-ago"> ... </div>
Although this setup functions correctly, I notice many date-fns–related JS requests being fired in the browser’s Network tab—each file is loaded separately (even when the time-ago controller isn’t present) and none are fingerprinted for proper caching. This eventually leads to issues such as "Too many requests" due to repeated downloads, especially with security measures like rack-attack in place.
How can I improve this? What is the recommended approach for handling such modular JS libraries in Rails using Importmap so that unnecessary HTTP requests are avoided and caching/fingerprinting issues are resolved?
Comments 0
•Answers 3
•Views 67
Answer by TychoAI Agent • 1 month ago
When using Importmap, you’re effectively asking the browser to load each ES module individually rather than getting a single bundled file. That’s why—even though you only need one function plus a couple of locales—the whole dependency tree is being walked and loaded as separate HTTP requests. Here are a few strategies to address this:
Instead of copying the entire library into your app, you can pin the package from a CDN that provides a bundled version (or at least one with fewer individual module files). For example, if the CDN offers a build that includes only the functionality you need, you’d update your config/importmap.rb
to point to that URL. This often comes with fingerprinting and better caching.
Example:
RUBY# config/importmap.rb pin "date-fns", to: "https://cdn.jsdelivr.net/npm/date-fns@2.29.3/esm/index.js"
(Note: Replace the URL with one that matches the functionality you require.)
Since you only require one function and two locales, you can create a minimal bundle manually using a bundler (such as esbuild). You’d bundle the required parts of date-fns
into a single JavaScript file. Then pin that bundle in your import map to avoid the many individual requests.
For example, first create a bundle (you might create a file like app/javascript/vendor/date-fns-bundle.js
):
JAVASCRIPT// date-fns-bundle.js import { formatDistanceToNow } from "date-fns"; import { it } from "date-fns/locale/it"; import { enUS } from "date-fns/locale/en-US"; export { formatDistanceToNow, it, enUS };
Then adjust your import map:
RUBY# config/importmap.rb pin "date-fns-bundle", to: "vendor/date-fns-bundle.js"
And in your Stimulus controller, import from your custom bundle:
JAVASCRIPTimport { Controller } from "@hotwired/stimulus" import { formatDistanceToNow, it, enUS } from "date-fns-bundle" // Connects to data-controller="time-ago" export default class extends Controller { // ... }
This way, all the code you need is delivered in one HTTP request, reducing network overhead and fingerprinting issues.
Rails 8 supports both Importmap and bundling strategies (using esbuild, webpack, or similar). If you find yourself needing finer control over asset delivery and caching (or want to avoid the browser’s multiple requests for modules), you might decide to switch to a bundling approach for your JavaScript. Bundlers will combine JS files into one or a few bundles which are fingerprinted for caching. However, if you prefer Importmap for other reasons, then one of the first two options is likely preferable.
Why the many requests?
Importmap just maps module names to URLs. When a module (like your pinned date-fns
) imports its own dependencies, the browser issues separate HTTP requests for each.
How to improve it?
Either switch to a CDN distribution that provides a bundled version with fingerprinted URLs or create your own minimal bundle that you can pin. Both approaches reduce the number of HTTP requests and allow for better caching.
By adjusting your approach using one of these strategies, you’ll eliminate the overhead of loading many separate files and avoid the “Too many requests” issues caused by your dependency tree.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by QuantumStargazer487 • 1 month ago
Proposed solution
A more viable way to make things work better than as described originally in the question is to bundle (using esbuild
locally) the date-fns
JS library with only the specific modules needed (formatDistanceToNow
, it
, and enUS
for the specific case), then use that bundle (one file) in Rails for consume through Importmap.
RUBY# config/importmap.rb // ... pin 'date-fns-formatDistanceToNow', to: 'vendor/date-fns-formatDistanceToNow.js'
JAVASCRIPT# app/javascript/controllers/time_ago_controller.js import { Controller } from "@hotwired/stimulus" import { formatDistanceToNow, it, enUS } from "date-fns-formatDistanceToNow"; // Connects to data-controller="time-ago" export default class extends Controller { // ... }
HTML# show.html.erb <div data-controller="time-ago"> ... </div>
Although it costs a bit of extra work (even in the future for maintenance!), making this all concerns are solved (caching, fingerprinting, and the amount of requests that is reduced to one request).
No comments yet.
Answer by LunarWayfarer151 • 1 month ago
Using a CDN url would be the simplest option, because there are a lot of relative imports that would be resolved automatically relative to CDN:
BASHpin "formatDistanceToNow", to: "https://ga.jspm.io/npm:date-fns@4.1.0/formatDistanceToNow.js"
If you're feeling iffy about all those requests, use jsdelivr, it seems to do a different kind of bundle for ESM by inlining all the imports:
https://cdn.jsdelivr.net/npm/date-fns@4.1.0/formatDistanceToNow.js/+esm
compared to
https://ga.jspm.io/npm:date-fns@4.1.0/formatDistanceToNow.js
(jspm does flatten all the dependent imports into this one file, so you don't end up with a lot of sequential requests)
BASH# NOTE: /+esm at the end of the url pin "formatDistanceToNow", to: "https://cdn.jsdelivr.net/npm/date-fns@4.1.0/formatDistanceToNow.js/+esm"
This could get rather bloated if you want to import several functions.
Small files and many requests is not a bad thing, it's good for caching and parallel downloads.
Currently, there is no easy way to use importmap-rails
for downloaded modules if they have other imports, as jspm api doesn't provide any information on dependencies (at least, not always). If we're to stick to a true importmap aproach, some manual work is required to resolve dependencies:
RUBY# pin.rb require_relative "./config/application" require "importmap/packager" def download url, path file_path = Pathname.new("vendor/javascript/#{path}") # => "vendor/javascript/date-fns/constructNow.js" file_path.parent.mkpath # ensure intermediate directories exist file_path.write(Net::HTTP.get_response(URI(url)).body) end package = ARGV.first packager = Importmap::Packager.new json = packager.import(package) # => {"date-fns/formatDistanceToNow" => "https://ga.jspm.io/npm:date-fns@4.1.0/formatDistanceToNow.js"} json.each do |name, url| # download the main file download(url, "#{name}.js") puts %(pin "#{name}", to: "#{name}.js") base_url = url.match(/.+\/).to_s.chomp("/") # => "https://ga.jspm.io/npm:date-fns@4.1.0" content = Net::HTTP.get_response(URI(url)).body # => "import{constructNow as o}from\"./constructNow.js\";import{formatDistance as t}from\"./formatDistance.js\"; ... imports = content.scan(/import.*?"(.+?)"/).flatten # => ["./constructNow.js", "./formatDistance.js", "./constructFrom.js", "./constants.js", "./_/D4S1TYKo.js", ... imports.each do |import| url = "#{base_url}/#{import.remove(/^\./)}" # => "https://ga.jspm.io/npm:date-fns@4.1.0/constructNow.js" module_name = name.split("/").first # => "date-fns" path = import.sub(/^\./, module_name) # => "date-fns/constructNow.js" # download dependencies download(url, path) puts %(pin "/assets/#{path}", to: "#{path}") end end
BASH$ ruby ./pin.rb date-fns/formatDistanceToNow # copy paste these into importmap.rb pin "date-fns/formatDistanceToNow", to: "date-fns/formatDistanceToNow.js" pin "/assets/date-fns/constructNow.js", to: "date-fns/constructNow.js" pin "/assets/date-fns/formatDistance.js", to: "date-fns/formatDistance.js" pin "/assets/date-fns/constructFrom.js", to: "date-fns/constructFrom.js" pin "/assets/date-fns/constants.js", to: "date-fns/constants.js" pin "/assets/date-fns/_/D4S1TYKo.js", to: "date-fns/_/D4S1TYKo.js" pin "/assets/date-fns/locale/en-US.js", to: "date-fns/locale/en-US.js" pin "/assets/date-fns/_/BHOttRZ-.js", to: "date-fns/_/BHOttRZ-.js" pin "/assets/date-fns/_/BkvIkgYg.js", to: "date-fns/_/BkvIkgYg.js" pin "/assets/date-fns/_/BnFQDOKz.js", to: "date-fns/_/BnFQDOKz.js" pin "/assets/date-fns/_/xou8AGNM.js", to: "date-fns/_/xou8AGNM.js" pin "/assets/date-fns/_/DCvnqCw9.js", to: "date-fns/_/DCvnqCw9.js" pin "/assets/date-fns/toDate.js", to: "date-fns/toDate.js" pin "/assets/date-fns/_/Dntog4x-.js", to: "date-fns/_/Dntog4x-.js" pin "/assets/date-fns/compareAsc.js", to: "date-fns/compareAsc.js" pin "/assets/date-fns/differenceInMonths.js", to: "date-fns/differenceInMonths.js" pin "/assets/date-fns/differenceInCalendarMonths.js", to: "date-fns/differenceInCalendarMonths.js" pin "/assets/date-fns/isLastDayOfMonth.js", to: "date-fns/isLastDayOfMonth.js" pin "/assets/date-fns/endOfDay.js", to: "date-fns/endOfDay.js" pin "/assets/date-fns/endOfMonth.js", to: "date-fns/endOfMonth.js" pin "/assets/date-fns/differenceInSeconds.js", to: "date-fns/differenceInSeconds.js" pin "/assets/date-fns/_/D-sZk5CW.js", to: "date-fns/_/D-sZk5CW.js" pin "/assets/date-fns/differenceInMilliseconds.js", to: "date-fns/differenceInMilliseconds.js"
pin "/assets/...
allows for relative imports, these have to match an importmap otherwise imports will break in production when using sprockets and won't work at all with propshaft.
This looks worse, because local server is running http/1.1 and, well, ruby. However, we're preloading all those files without waiting for formatDistanceToNow.js
to download, unlike option 1.
Subsequent requests look identical and very cached:
No comments yet.
No comments yet.