Browse Source

perf(projects): Correctly lazy load project cards [UP-212] (#41508)

Orgs with many projects have an unusable projects page due to the number
of rendered elements. There was a LazyLoad component, but it incorrectly
wrapped the entire results div, not individual project cards. This
rectifies that and makes the projects page usable again.

We should still probably paginate that page, but this is a quick fix
that at least makes things tenable in the meantime.
Malachi Willey 2 years ago
parent
commit
48610cdf72

+ 11 - 0
static/app/__mocks__/react-lazyload.tsx

@@ -0,0 +1,11 @@
+/**
+ * Auto-mock of the react-lazyload library for jest
+ *
+ * These mocks are simple no-ops to make testing lazy-loaded components simpler.
+ */
+
+const LazyLoad = ({children}) => children;
+
+export const forceCheck = jest.fn();
+
+export default LazyLoad;

+ 35 - 15
static/app/views/projectsDashboard/index.tsx

@@ -1,5 +1,5 @@
 import {Fragment, useEffect, useMemo, useState} from 'react';
-import LazyLoad from 'react-lazyload';
+import LazyLoad, {forceCheck} from 'react-lazyload';
 import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 import {withProfiler} from '@sentry/react';
@@ -21,8 +21,9 @@ import {IconAdd, IconUser} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import ProjectsStatsStore from 'sentry/stores/projectsStatsStore';
 import space from 'sentry/styles/space';
-import {Organization, TeamWithProjects} from 'sentry/types';
+import {Organization, Project, TeamWithProjects} from 'sentry/types';
 import {sortProjects} from 'sentry/utils';
+import useOrganization from 'sentry/utils/useOrganization';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
 import withTeamsForUser from 'sentry/utils/withTeamsForUser';
@@ -40,6 +41,37 @@ type Props = {
   teams: TeamWithProjects[];
 } & RouteComponentProps<{orgId: string}, {}>;
 
+function ProjectCardList({projects}: {projects: Project[]}) {
+  const organization = useOrganization();
+  const hasProjectAccess = organization.access.includes('project:read');
+
+  // By default react-lazyload will only check for intesecting components on scroll
+  // This forceCheck call is necessary to recalculate when filtering projects
+  useEffect(() => {
+    forceCheck();
+  }, [projects]);
+
+  return (
+    <ProjectCards>
+      {sortProjects(projects).map(project => (
+        <LazyLoad
+          debounce={50}
+          height={330}
+          offset={400}
+          unmountIfInvisible
+          key={project.slug}
+        >
+          <ProjectCard
+            data-test-id={project.slug}
+            project={project}
+            hasProjectAccess={hasProjectAccess}
+          />
+        </LazyLoad>
+      ))}
+    </ProjectCards>
+  );
+}
+
 function Dashboard({teams, organization, loadingTeams, error, router, location}: Props) {
   useEffect(() => {
     return function cleanup() {
@@ -62,7 +94,6 @@ function Dashboard({teams, organization, loadingTeams, error, router, location}:
 
   const canCreateProjects = organization.access.includes('project:admin');
   const canJoinTeam = organization.access.includes('team:read');
-  const hasProjectAccess = organization.access.includes('project:read');
 
   const selectedTeams = getTeamParams(location ? location.query.team : '');
   const filteredTeams = teams.filter(team => selectedTeams.includes(team.id));
@@ -163,18 +194,7 @@ function Dashboard({teams, organization, loadingTeams, error, router, location}:
                   query={projectQuery}
                 />
               </SearchAndSelectorWrapper>
-              <LazyLoad once debounce={50} height={300} offset={300}>
-                <ProjectCards>
-                  {sortProjects(filteredProjects).map(project => (
-                    <ProjectCard
-                      data-test-id={project.slug}
-                      key={project.slug}
-                      project={project}
-                      hasProjectAccess={hasProjectAccess}
-                    />
-                  ))}
-                </ProjectCards>
-              </LazyLoad>
+              <ProjectCardList projects={filteredProjects} />
             </Layout.Main>
           </Body>
           {showResources && <Resources organization={organization} />}

+ 1 - 0
static/app/views/projectsDashboard/projectCard.tsx

@@ -321,6 +321,7 @@ const HeaderRow = styled('div')`
 
 const StyledProjectCard = styled(Panel)`
   min-height: 330px;
+  margin: 0;
 `;
 
 const FooterWrapper = styled('div')`

+ 0 - 4
tests/js/setup.ts

@@ -91,10 +91,6 @@ jest.mock('react-router', function reactRouterMockFactory() {
     },
   };
 });
-jest.mock('react-lazyload', function reactLazyLoadMockFactory() {
-  const LazyLoadMock = ({children}) => children;
-  return LazyLoadMock;
-});
 
 jest.mock('react-virtualized', function reactVirtualizedMockFactory() {
   const ActualReactVirtualized = jest.requireActual('react-virtualized');