The garbage you never had to create
Ruby's collector is fast. The cheapest object is still the one you didn't allocate, and in hot paths that difference is measurable.
Most Ruby code should not think about allocation. In a hot path — a loop over a large collection, a serialiser, a parser — it becomes the dominant cost, and it shows up as diffuse GC time rather than an obvious hotspot.
Where the garbage comes from
Every string literal in Ruby creates a new object each time it is evaluated. In a method called a million times, that is a million strings that exist only to be discarded.
The frozen_string_literal: true magic comment makes literals in that file frozen and
shared. It is a one-line change per file, and on string-heavy code the reduction in
allocations is substantial. Ruby has been moving toward this being the default, so
adopting it also future-proofs the file.
The caveat is that mutating a frozen string raises. Code doing str << "more" on a
literal will break, which is exactly the code that most needed changing.
Building strings without building garbage
Concatenation with + creates a new string per operation. In a loop that is quadratic
in garbage.
Appending with << mutates in place and creates nothing. For assembling output, that
difference is the whole game. Interpolation is fine — it builds one string — but
repeated interpolation inside a loop is not.
Enumerable chains and intermediate arrays
Each of map, select and reject returns a new array. A three-method chain over a
large collection holds three full copies.
lazy avoids the intermediates by passing elements through one at a time.
each_with_object avoids them by building one result. filter_map does select and map
in a single pass, which is the common case and one array instead of two.
The fastest object is the one you never allocated, and the second fastest is the one you allocated once.
Symbols, hashes and small wins that add up
Symbols are not garbage collected in the way strings are and are the right key type for known, fixed sets. Dynamically creating symbols from user input is the opposite — that is unbounded growth.
Hash lookups with string keys allocate when the key is built per call. Frozen constants for repeated keys remove that.
And each instead of map when you are ignoring the return value costs nothing and
saves the array.
Measure, do not assume
All of this is worth doing in the small number of places that run constantly, and worth ignoring everywhere else. Micro-optimising a controller action that runs once per request is wasted effort and makes the code worse.
An allocation profiler tells you which lines create the most objects. That list is usually short, frequently surprising, and it is the only sensible place to start.