diff --git a/changelogs/unreleased/dm-api-projects-members-preload.yml b/changelogs/unreleased/dm-api-projects-members-preload.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e04e7c37d13415c24459f930a9c3428325cb33cd
--- /dev/null
+++ b/changelogs/unreleased/dm-api-projects-members-preload.yml
@@ -0,0 +1,6 @@
+---
+title: Only preload member records for the relevant projects/groups/user in projects
+  API
+merge_request:
+author:
+type: performance
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 49cd4fccc637713f311d6aa7b2300cda36061e5a..c4537036a3a96c0c7cc7863d9d10bd595105112f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -832,8 +832,8 @@ module API
     class ProjectWithAccess < Project
       expose :permissions do
         expose :project_access, using: Entities::ProjectAccess do |project, options|
-          if options.key?(:project_members)
-            (options[:project_members] || []).find { |member| member.source_id == project.id }
+          if options[:project_members]
+            options[:project_members].find { |member| member.source_id == project.id }
           else
             project.project_member(options[:current_user])
           end
@@ -841,8 +841,8 @@ module API
 
         expose :group_access, using: Entities::GroupAccess do |project, options|
           if project.group
-            if options.key?(:group_members)
-              (options[:group_members] || []).find { |member| member.source_id == project.namespace_id }
+            if options[:group_members]
+              options[:group_members].find { |member| member.source_id == project.namespace_id }
             else
               project.group.group_member(options[:current_user])
             end
@@ -853,13 +853,24 @@ module API
       def self.preload_relation(projects_relation, options = {})
         relation = super(projects_relation, options)
 
-        unless options.key?(:group_members)
-          relation = relation.preload(group: [group_members: [:source, user: [notification_settings: :source]]])
+        # MySQL doesn't support LIMIT inside an IN subquery
+        if Gitlab::Database.mysql?
+          project_ids = relation.pluck('projects.id')
+          namespace_ids = relation.pluck(:namespace_id)
+        else
+          project_ids = relation.select('projects.id')
+          namespace_ids = relation.select(:namespace_id)
         end
 
-        unless options.key?(:project_members)
-          relation = relation.preload(project_members: [:source, user: [notification_settings: :source]])
-        end
+        options[:project_members] = options[:current_user]
+          .project_members
+          .where(source_id: project_ids)
+          .preload(:source, user: [notification_settings: :source])
+
+        options[:group_members] = options[:current_user]
+          .group_members
+          .where(source_id: namespace_ids)
+          .preload(:source, user: [notification_settings: :source])
 
         relation
       end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 8871792060bb5c74cd161de46621c6c83692e981..3ef3680c5d9dbdd4092ff593f1d46b79a71780b2 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -58,16 +58,9 @@ module API
         projects = paginate(projects)
         projects, options = with_custom_attributes(projects, options)
 
-        if current_user
-          project_members = current_user.project_members.preload(:source, user: [notification_settings: :source])
-          group_members = current_user.group_members.preload(:source, user: [notification_settings: :source])
-        end
-
         options = options.reverse_merge(
           with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
           statistics: params[:statistics],
-          project_members: project_members,
-          group_members: group_members,
           current_user: current_user
         )
         options[:with] = Entities::BasicProjectDetails if params[:simple]