Project

General

Profile

Actions

Defect #44350

open

Project overview page is slow when the members box contains many members

Added by Takenori TAKAKI about 1 month ago. Updated 1 day ago.

Status:
New
Priority:
Normal
Category:
Performance
Target version:
Resolution:
Affected version:

Description

Problem

When a project has many members, the overview page (/projects/:id) becomes slow because the members box loads and renders all member links on the initial page load.
In a local development test with a project that has about 4000 members, the initial overview page load took 1345 ms.

Proposal

To address this, I propose limiting the initial display to the first 50 members per role and providing a "Show all" link to load the remaining members for that role on demand.
This preserves the current role-based structure, keeps all members accessible from the overview page itself, and avoids loading all principals up front for the initial render.

In the same local development test, the initial overview page load improved to about 32-78 ms after this change.
Loading all remaining members still has a cost, but that cost is paid only when "Show all" is explicitly requested.


Files

members-box-show-all.png (672 KB) members-box-show-all.png Takenori TAKAKI, 2026-08-18 14:26
members-box-show-all.patch (18.8 KB) members-box-show-all.patch Takenori TAKAKI, 2026-08-18 14:27
members-box-show-all-v2.patch (17.6 KB) members-box-show-all-v2.patch Takenori TAKAKI, 2026-09-10 15:02
extra-space-before-comma-after-show-all.png (32.4 KB) extra-space-before-comma-after-show-all.png Katsuya HIDAKA, 2026-09-11 06:32
members-box-show-all-v3.patch (18.9 KB) members-box-show-all-v3.patch Takenori TAKAKI, 2026-09-14 13:53
members-box-show-all-v4.patch (19 KB) members-box-show-all-v4.patch Takenori TAKAKI, 2026-09-15 03:35
members-box-show-all-v5.patch (18.4 KB) members-box-show-all-v5.patch Takenori TAKAKI, 2026-09-15 09:01
members-box-show-all-v6.patch (17.7 KB) members-box-show-all-v6.patch Takenori TAKAKI, 2026-09-15 15:29
clipboard-202609170152-leohr.png (49 KB) clipboard-202609170152-leohr.png Marius BĂLTEANU, 2026-09-17 00:52

Related issues

Related to Redmine - Defect #5942: Groups with a few thousand users gives issues in a few UIs and slow-downs on a few screensNew2010-07-22Actions
Related to Redmine - Feature #44458: Filter project members by name and role in project settingsClosedMarius BĂLTEANUActions
Related to Redmine - Patch #2198: print short list of users in overview page of projectNew2008-11-15Actions
Related to Redmine - Defect #27902: Overview page is very slow for private projects with large number of membersNewActions
Actions #1

Updated by Holger Just about 1 month ago

  • Related to Defect #5942: Groups with a few thousand users gives issues in a few UIs and slow-downs on a few screens added
Actions #2

Updated by Takenori TAKAKI 20 days ago

Thank you Holger Just for linking the related issue.

Attached an updated patch ( members-box-show-all-v2.patch ). Same behaviour and appearance as the first one, with lighter DB access:

  • Project#principals_for_role pages the members with an IN (subquery) and SQL
    OFFSET / LIMIT, instead of loading every member id of the role in Ruby to
    build the first page.
  • Project#roles_with_active_members uses the same form (one query instead of two).
  • "Show all" now uses a dedicated Stimulus controller rather than a generic one.

Measured on the project overview of a project with ~4000 members (development,
SQLite, warm):

unpatched:  ~1260 ms  (views ~270 ms, DB ~100 ms, GC ~310 ms)
patched: ~40 ms (views ~25 ms, DB ~5 ms, GC ~6 ms)

Clicking "Show all" for a role with ~4000 members then loads the rest of that role in ~250 ms, and that cost is only paid on demand.

Actions #3

Updated by Katsuya HIDAKA 19 days ago

Currently, opening a project from the project list displays the project overview page. For projects with many members, this can make opening the project slow every time. I think this change addresses the issue well.

I reviewed the code and tested the patch. I confirmed that all tests pass and tested the behavior on Chrome, Safari, and Firefox. I also confirmed that the members box shows the same members before and after applying the patch.

Looks good to me overall. I noticed just two minor issues:

  1. As shown in the attached screenshot, there is an extra space before the comma where the “Show all” link was.
  2. roles_with_active_members can be loaded in advance to avoid running two queries, one for any? and another when iterating over the roles. This is a minor optimization, but the following change reduces them to one query:
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a7bda382c..9f22dbf67 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -175,7 +175,7 @@ class ProjectsController < ApplicationController

     respond_to do |format|
       format.html do
-        @roles_with_active_members = @project.roles_with_active_members
+        @roles_with_active_members = @project.roles_with_active_members.load
         @subprojects = @project.leaf? ? [] : @project.children.visible.to_a
         @news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
         with_subprojects = Setting.display_subprojects_issues?
Actions #4

Updated by Katsuya HIDAKA 19 days ago

#note-3

2. roles_with_active_members can be loaded in advance to avoid running two queries, one for any? and another when iterating over the roles.

Before adding load, I confirmed that the following two queries were executed:

Role Exists?
↳ app/views/projects/_members_box.html.erb:2
Role Load
↳ app/views/projects/_members_box.html.erb:5

After adding load, only one query is executed:

Role Load
↳ app/controllers/projects_controller.rb:178
Actions #5

Updated by Marius BĂLTEANU 16 days ago

  • Assignee set to Marius BĂLTEANU
  • Target version set to 7.1.0
Actions #6

Updated by Takenori TAKAKI 16 days ago

Thank you for the review!

Fixed both:

  • `#show` now loads `roles_with_active_members` explicitly (`.load`), so the
    members box no longer issues a separate EXISTS query before the Load query —
    back to one query.
  • The space separating the last member from "Show all" was outside the trigger
    element. Clicking "Show all" removed the trigger but left that space behind,
    stranding it before the comma of the appended members. Moved the space inside
    the trigger so it's removed along with it.

Attached an updated patch (members-box-show-all-v3.patch).

Actions #7

Updated by Takenori TAKAKI 16 days ago

Sorry, my previous patch didn't fully fix the spacing issue.
Attached an updated patch (members-box-show-all-v4.patch), replacing the previous one.

Actions #8

Updated by Katsuya HIDAKA 15 days ago

I confirmed that the patch mentioned in #note-7 fixes the issues mentioned in #note-3.

However, I have two minor suggestions. First, I think _members_box_role.html.erb is a bit hard to read because all line breaks in the code have been removed. Second, it would be safer to use safe_join instead of join(", ").html_safe in _members_box_principals.html.erb.

Here is a proposal patch to address these issues. What do you think?

diff --git a/app/views/projects/_members_box_principals.html.erb b/app/views/projects/_members_box_principals.html.erb
index 5221251b0..165eefdab 100644
--- a/app/views/projects/_members_box_principals.html.erb
+++ b/app/views/projects/_members_box_principals.html.erb
@@ -1 +1,9 @@
-<%= principals.collect {|p| link_to_principal(p, :class => p.is_a?(Group) ? 'icon icon-group' : nil)}.join(", ").html_safe %>
\ No newline at end of file
+<%= safe_join(principals.map {|p| link_to_principal(p, :class => p.is_a?(Group) ? 'icon icon-group' : nil)}, ", ") -%>
+<% if display_more %>
+<span class="show-all-members-trigger" data-project-members-box-expand-target="trigger">
+  <%= link_to(sprite_icon('angle-right', l(:label_show_all), rtl: true),
+              show_all_members_project_path(@project, :role_id => role.id, :offset => principals.size),
+              :class => 'icon icon-angle-right',
+              :data => {:action => 'click->project-members-box-expand#showAll'}) %>
+</span>
+<% end %>
diff --git a/app/views/projects/_members_box_role.html.erb b/app/views/projects/_members_box_role.html.erb
index 4904ca884..952207375 100644
--- a/app/views/projects/_members_box_role.html.erb
+++ b/app/views/projects/_members_box_role.html.erb
@@ -3,5 +3,5 @@
 %>
 <p class="member-role" data-controller="project-members-box-expand">
   <span class="label"><%= role %>:</span>
-  <%= render :partial => 'members_box_principals', :locals => {:principals => principals} %><% if more %><span class="show-all-members-trigger" data-project-members-box-expand-target="trigger"> <%= link_to(sprite_icon('angle-right', l(:label_show_all), rtl: true), show_all_members_project_path(@project, :role_id => role.id, :offset => principals.size), :class => 'icon icon-angle-right', :data => {:action => 'click->project-members-box-expand#showAll'}) %></span><% end %>
+  <%= render :partial => 'members_box_principals', :locals => {:principals => principals, :role => role, :display_more => more} %>
 </p>
diff --git a/app/views/projects/show_all_members.html.erb b/app/views/projects/show_all_members.html.erb
index 437c38c1e..53dea58b1 100644
--- a/app/views/projects/show_all_members.html.erb
+++ b/app/views/projects/show_all_members.html.erb
@@ -1 +1,3 @@
-<% if @principals.any? %>, <%= render :partial => 'members_box_principals', :locals => {:principals => @principals} %><% end %>
+<% if @principals.any? %>
+, <%= render :partial => 'members_box_principals', :locals => {:principals => @principals, :role => @role, :display_more => false} %>
+<% end %>
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index 9c67a4dfd..bd2ace519 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -967,19 +967,6 @@ class ProjectsControllerTest < Redmine::ControllerTest
     end
   end

-  def test_show_should_keep_the_separating_space_inside_the_show_all_trigger
-    project = Project.find(1)
-    role = Role.find(1)
-    (Project::MEMBERS_BOX_PRINCIPALS_PER_ROLE + 5).times {Member.create!(:principal => User.generate!, :project => project, :role_ids => [role.id])}
-
-    get(:show, :params => {:id => 'ecookbook'})
-    assert_response :success
-    # The separator space must be inside the trigger, not a sibling text node,
-    # or clicking "Show all" leaves it stranded before the comma.
-    assert_no_match(/<\/a>\s+<span class="show-all-members-trigger"/, response.body)
-    assert_match(/<span class="show-all-members-trigger"[^>]*> <a /, response.body)
-  end
-
   def test_show_all_members_should_return_every_member_from_the_given_offset
     project = Project.find(1)
     role = Role.find(1)
Actions #9

Updated by Takenori TAKAKI 15 days ago

Thanks Katsuya HIDAKA, this is a nice improvement — much more readable, and
safe_join is a good call. Confirmed it works well.
Attached an updated patch (members-box-show-all-v5.patch) with this applied.

Actions #10

Updated by Katsuya HIDAKA 15 days ago

I reviewed the changes in the v5 patch and confirmed that it works as expected. Looks good to me.

I noticed that the following test seems to fail on MySQL. Could you check it?

Failure:
ProjectsControllerTest#test_show_should_load_roles_with_active_members_to_avoid_a_duplicate_query [test/functional/projects_controller_test.rb:932]:
Expected: 1
  Actual: 0

bin/rails test test/functional/projects_controller_test.rb:920

Thanks for your work on this!

Actions #11

Updated by Takenori TAKAKI 15 days ago

Thanks Katsuya HIDAKA.
That test only asserted the query count for the roles_with_active_members optimization;
its regexp assumed double-quoted identifiers and missed MySQL's backtick quoting.
I confirmed locally that MySQL also runs one query with `.load`,
so I removed the test rather than fixing the regexp — it wasn't testing behavior,
and matching raw SQL text felt too fragile to keep around.
Attached an updated patch (members-box-show-all-v6.patch).

Actions #12

Updated by Katsuya HIDAKA 14 days ago

I confirmed that all tests, including the MySQL tests, passed with the v6 patch. LGTM.

Thank you for your work on this!

Actions #13

Updated by Marius BĂLTEANU 14 days ago

  • Related to Feature #44458: Filter project members by name and role in project settings added
Actions #14

Updated by Marius BĂLTEANU 14 days ago

Takenori TAKAKI, Katsuya HIDAKA, thanks for you work on this. I agree that we should fix it one way or another.

First of all, the "load more" mechanism works as expected and fixes the performance issue, but I still think that showing 50 members per each role from the begging is not very useful (you still need to search for name using browser search).

I propose a slightly different approach:

Each role shows its first 5 or 10 members with their avatar, followed by a badge with the number of members that are not displayed:

The badge links to the members tab of the project settings, filtered on that role (#44458). That tab gets a filter form (user or group name, and role), so the link lands directly on the members of that role, where they can also be searched and paginated.

"View all members" links to the same members tab, but without any filter.

In order to work also for users without "Manage members" permission, we need to make the members list available outside of the project settings. One ideea is to render the same list under the overview page for users without permission and under Settings -> Members for users with "Manage members" permission.

Actions #15

Updated by Go MAEDA 10 days ago

  • Related to Patch #2198: print short list of users in overview page of project added
Actions #16

Updated by Go MAEDA 10 days ago

  • Related to Defect #27902: Overview page is very slow for private projects with large number of members added
Actions #17

Updated by Takenori TAKAKI 1 day ago

Thanks, Marius BĂLTEANU. Your proposal in #note-14 sounds like a better UX improvement than my flat list capped at N.

That said, as you mentioned, it needs work on permissions, avatar rendering, and a filter UI, so it'll take a bit more time to get right.

This performance problem itself has been reported more than once (#2198, #27902), and #2198 has been open for years.

I'd suggest keeping this issue focused on the performance fix that doesn't change the current experience much (the patch in #note-11), and treating your proposal as a separate UX-improvement issue.

Actions

Also available in: Atom PDF