Skip to content

Optimize can?#887

Open
tr4b4nt wants to merge 4 commits intoCanCanCommunity:developfrom
tr4b4nt:develop
Open

Optimize can?#887
tr4b4nt wants to merge 4 commits intoCanCanCommunity:developfrom
tr4b4nt:develop

Conversation

@tr4b4nt
Copy link

@tr4b4nt tr4b4nt commented Dec 20, 2025

Hi, we are using CanCanCan in rails project with 200+ ability rules and there are many pages that do many can? checks when being rendered. We noticed that the can? checks take up significant part of our response time.

Here is attempt to speed things up. From our testing, the most significant change would be to cache and shorten subject.ancestors in alternative_subjects but that would be bigger change and might break some edge case usage of this gem so it is not included.

As you can see below, speedup varies between 20-70%. Every commit includes different optimization and can be reverted if the change is not ok.

Simple benchmark (which just creates small and big ability class with some classes and checks). It attempts to simulate different types of abilities and workflow but is very basic and definitely could be improved:

require "cancancan"
require "benchmark/ips"
require "active_record"

$classes = 50.times.map { Class.new } + 50.times.map { Class.new(ActiveRecord::Base) }
$classes.flat_map { Class.new(_1) }
$first_last_classes = [$classes.first, $classes.last]
class BigAbility
  include CanCan::Ability

  def initialize
    alias_action :index, :show, :search, to: :read
    $classes.shuffle(random: Random.new(1)).each do |klass|
      can [:index, :create], klass
    end
    100.times do |i|
      can :read, :"custom#{i}"
    end
    $classes.shuffle(random: Random.new(1)).each do |klass|
      can [:delete], klass
    end
  end
end

class SmallAbility
  include CanCan::Ability

  def initialize
    $first_last_classes.each do |klass|
      can :read, klass
    end
  end
end

Benchmark.ips do |x|
  x.report("single_check_big") { ability = BigAbility.new; $classes.each { |klass| ability.can?(:read, klass) } }
  x.report("multi_checks_big") { ability = BigAbility.new; $first_last_classes.each { |klass| 10.times { ability.can?(:read, klass) } } }
  x.report("single_check_small") { ability = SmallAbility.new; $classes.each { |klass| ability.can?(:read, klass) } }
  x.report("multi_checks_small") { ability = SmallAbility.new; $first_last_classes.each { |klass| 10.times { ability.can?(:read, klass) } } }
end; 0

Results:
CanCanCan 3.6.0:

ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-linux]
Warming up --------------------------------------
    single_check_big    41.000 i/100ms
    multi_checks_big   104.000 i/100ms
  single_check_small    90.000 i/100ms
  multi_checks_small   297.000 i/100ms
Calculating -------------------------------------
    single_check_big    422.154 (± 6.4%) i/s    (2.37 ms/i) -      2.132k in   5.079771s
    multi_checks_big      1.036k (± 1.0%) i/s  (965.03 μs/i) -      5.200k in   5.018654s
  single_check_small    901.486 (± 2.1%) i/s    (1.11 ms/i) -      4.590k in   5.093925s
  multi_checks_small      2.942k (± 0.9%) i/s  (339.87 μs/i) -     14.850k in   5.047496s

These changes:

ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-linux]
Warming up --------------------------------------
    single_check_big    58.000 i/100ms
    multi_checks_big   123.000 i/100ms
  single_check_small   157.000 i/100ms
  multi_checks_small   482.000 i/100ms
Calculating -------------------------------------
    single_check_big    575.897 (± 4.7%) i/s    (1.74 ms/i) -      2.900k in   5.049236s
    multi_checks_big      1.227k (± 2.0%) i/s  (814.67 μs/i) -      6.150k in   5.012142s
  single_check_small      1.570k (± 1.0%) i/s  (636.81 μs/i) -      8.007k in   5.099492s
  multi_checks_small      4.855k (± 1.0%) i/s  (205.96 μs/i) -     24.582k in   5.063452s

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