Sitelet https://github.com/rack/rack/pull/2463
Skip to content

Only split and examine VARY header in Deflater when present - #2463

Open
p8 wants to merge 1 commit into
rack:mainfrom
p8:performance/deflater-vary
Open

p8 wants to merge 1 commit into
rack:mainfrom
p8:performance/deflater-vary

Conversation

@p8

@p8 p8 commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

When the VARY response header isn't set, there is no need to parse it and examine its content.

This improves the Deflater middleware by 20% when the header isn't set:

# frozen_string_literal: true

require 'benchmark/ips'
require 'rack'

module Rack
  class Deflater2 < Deflater
    VARY = 'vary'

    def call(env)
      status, headers, body = response = @app.call(env)

      unless should_deflate?(env, status, headers, body)
        return response
      end

      request = Request.new(env)

      encoding = Utils.select_best_encoding(%w(gzip identity),
                                            request.accept_encoding)

      # Set the Vary HTTP header.
      if headers.has_key?(VARY)
        vary = headers[VARY].to_s.split(",").map(&:strip)
        unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
          headers[VARY] = vary.push("Accept-Encoding").join(",")
        end
      else
        headers[VARY] = "Accept-Encoding"
      end

      case encoding
      when "gzip"
        headers['content-encoding'] = "gzip"
        headers.delete(CONTENT_LENGTH)
        mtime = headers["last-modified"]
        mtime = Time.httpdate(mtime).to_i if mtime
        response[2] = GzipStream.new(body, mtime, @sync)
        response
      when "identity"
        response
      else # when nil
        # Only possible encoding values here are 'gzip', 'identity', and nil
        message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
        bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
        [406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
      end
    end

  end
end

app = lambda do |env|
  [200, {}, []]
end

deflater = Rack::Deflater.new(app, {})
deflater2 = Rack::Deflater2.new(app, {})
request = Rack::MockRequest.env_for('', {})

Benchmark.ips do |x|
  x.config(warmup: 2, time: 5)

  x.report("main") do
    deflater.call(request)
  end

  x.report("branch") do
    deflater2.call(request)
  end
  x.compare!
end
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
                main    94.625k i/100ms
              branch   115.769k i/100ms
Calculating -------------------------------------
                main    950.471k (± 1.0%) i/s    (1.05 μs/i) -      4.826M in   5.077838s
              branch      1.173M (± 1.3%) i/s  (852.46 ns/i) -      5.904M in   5.034024s

Comparison:
              branch:  1173071.0 i/s
                main:   950471.1 i/s - 1.23x  slower

When the VARY response header isn't set, there is no need to parse it
and examine it's content.

This improves the Deflater middleware by 20% when the header isn't set:

```ruby

require 'benchmark/ips'
require 'rack'

module Rack
  class Deflater2 < Deflater
    VARY = 'vary'

    def call(env)
      status, headers, body = response = @app.call(env)

      unless should_deflate?(env, status, headers, body)
        return response
      end

      request = Request.new(env)

      encoding = Utils.select_best_encoding(%w(gzip identity),
                                            request.accept_encoding)

      # Set the Vary HTTP header.
      if headers.has_key?(VARY)
        vary = headers[VARY].to_s.split(",").map(&:strip)
        unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
          headers[VARY] = vary.push("Accept-Encoding").join(",")
        end
      else
        headers[VARY] = "Accept-Encoding"
      end

      case encoding
      when "gzip"
        headers['content-encoding'] = "gzip"
        headers.delete(CONTENT_LENGTH)
        mtime = headers["last-modified"]
        mtime = Time.httpdate(mtime).to_i if mtime
        response[2] = GzipStream.new(body, mtime, @sync)
        response
      when "identity"
        response
      else # when nil
        # Only possible encoding values here are 'gzip', 'identity', and nil
        message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
        bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
        [406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
      end
    end

  end
end

app = lambda do |env|
  [200, {}, []]
end

deflater = Rack::Deflater.new(app, {})
deflater2 = Rack::Deflater2.new(app, {})
request = Rack::MockRequest.env_for('', {})

Benchmark.ips do |x|
  x.config(warmup: 2, time: 5)

  x.report("main") do
    deflater.call(request)
  end

  x.report("branch") do
    deflater2.call(request)
  end
  x.compare!
end
```

```
ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin24]
Warming up --------------------------------------
                main    94.625k i/100ms
              branch   115.769k i/100ms
Calculating -------------------------------------
                main    950.471k (± 1.0%) i/s    (1.05 μs/i) -      4.826M in   5.077838s
              branch      1.173M (± 1.3%) i/s  (852.46 ns/i) -      5.904M in   5.034024s

Comparison:
              branch:  1173071.0 i/s
                main:   950471.1 i/s - 1.23x  slower
```
@p8
p8 force-pushed the performance/deflater-vary branch from 4ef9238 to 4316fe9 Compare May 5, 2026 11:07
@p8 p8 changed the title Only split and examine VARY header when present in Deflater Only split and examine VARY header in Deflater when present May 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant