A boy was given a knife. He was told "this is a sharp knife," but then when he tried to cut something with it, like a piece of meat or something, it didn't really work that well. And so the boy thought, "this knife isn't actually very good," but then the person who gave him the knife said "here...tilt it a little bit and do this other obscure thing with the way you're holding the knife", and lo and behold...the knife cut the meat.
Life and open source software sometimes give us knives that seem dull until we learn how to use them properly. And sometimes, knives just legitimately have problems with them. And sometimes, internet tutorials just give us bad advice on how to use the knife, but maybe at one point it was good advice. Life is complicated, so who knows, but in any case the knife I'm talking about is Rails.application.routes.url_helpers.
It's very common to generate URLs outside of the Rails controller context, and so it's very common to need the functionality of this module in a place where it doesn't come for free (a serializer, a job, etc). The internet has been innocently telling people for years to access the module directly, and this worked perfectly fine for a time.
But unfortunately, there are issues with using it this way in more recent versions of Rails (Github issue describing it, PR attempting to fix it). Per the fix description, every call to url_helpers generates a hefty new Module which is not cheap when invoked repeatedly. To illustrate the detriment of this, I set up a quick test Rails application. Here is the relevant code:
# routes.rb Rails.application.routes.draw do resources :things do collection do get :faster end end end # app/whatever/url_helper.rb class UrlHelper include Singleton include Rails.application.routes.url_helpers end # app/controllers/things_controller.rb class ThingsController < ApplicationController def index things_json = (1..100).map do |i| { id: i, url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost') } end render json: things_json end def faster things_json = (1..100).map do |i| { id: i, url: UrlHelper.instance.thing_url(i, host: 'localhost') } end render json: things_json end end
In the index action, we call the module directly in the code, per the StackOverflow advice. In the faster action, we use a helper class that includes the module, which seems to be how Rails would suggest that the module be used.
Let's see how these two approaches perform side-by-side. I ran a little test using ab and a single Rails instance (a lot of output omitted due to uselessness):
➜ ab -n 1000 http://127.0.0.1:3000/things Concurrency Level: 1 Time taken for tests: 18.155 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 4815000 bytes HTML transferred: 4485000 bytes Requests per second: 55.08 [#/sec] (mean) Time per request: 18.155 [ms] (mean) Time per request: 18.155 [ms] (mean, across all concurrent requests) Transfer rate: 259.00 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.1 0 1 Processing: 15 18 2.2 17 42 Waiting: 15 18 2.2 17 42 Total: 15 18 2.2 18 42
➜ ab -n 1000 http://127.0.0.1:3000/things/faster Concurrency Level: 1 Time taken for tests: 8.540 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 4815000 bytes HTML transferred: 4485000 bytes Requests per second: 117.09 [#/sec] (mean) Time per request: 8.540 [ms] (mean) Time per request: 8.540 [ms] (mean, across all concurrent requests) Transfer rate: 550.58 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.1 0 1 Processing: 7 8 1.3 8 23 Waiting: 7 8 1.3 8 23 Total: 7 8 1.3 8 23
In the first example (the index method) where we call the module directly, the request takes an average of 18 ms resulting in a throughput of 55 requests per second. "Not bad!" you might say, since all of your requests in production take 5 seconds on a good day. But what about when we simply include the module once instead of calling it directly? In that case (the faster method), the request takes an average of 8 ms resulting in a throughput of 117 requests per second, more than 2x as fast as the previous approach. Now I'm no chef, but that knife cuts meat adequately if you hold it the right way.
TL;DR don't call Rails.application.routes.url_helpers directly. include the module in your class and your code will be faster.