Skip to content

Commit c0fda7c

Browse files
authored
Improve instructor page performance with streaming and virtualization (#708)
1 parent 2992562 commit c0fda7c

16 files changed

Lines changed: 1509 additions & 1291 deletions

File tree

app/course/[course_id]/assignments/[assignment_id]/layout.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,10 @@ export default async function AssignmentLayout({
4848
}
4949
}
5050

51-
// Pre-fetch all assignment controller data on the server with caching
52-
const initialData = await fetchAssignmentControllerData(
53-
assignmentId,
54-
role.role === "instructor" || role.role === "grader"
55-
);
51+
// Keep instructor/grader assignment pages responsive for very large classes by
52+
// skipping heavyweight SSR prefetch. Students retain SSR prefetch for faster first paint.
53+
const isStaff = role.role === "instructor" || role.role === "grader";
54+
const initialData = isStaff ? undefined : await fetchAssignmentControllerData(assignmentId, false);
5655
return (
5756
<AssignmentProvider assignment_id={assignmentId} initialData={initialData}>
5857
{children}

app/course/[course_id]/layout.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ const ProtectedLayout = async ({
4545
redirect("/");
4646
}
4747

48-
// Pre-fetch all course controller data on the server with caching
49-
const initialData = await fetchCourseControllerData(Number.parseInt(course_id), user_role.role);
48+
// Staff pages should stream quickly even for very large classes; avoid blocking layout render
49+
// on a full table prefetch bundle.
50+
const shouldPrefetchCourseData = user_role.role === "student";
51+
const initialData = shouldPrefetchCourseData
52+
? await fetchCourseControllerData(Number.parseInt(course_id), user_role.role)
53+
: undefined;
5054

5155
// Get course information for timezone
5256
const course = await getCourse(Number.parseInt(course_id));

app/course/[course_id]/manage/assignments/[assignment_id]/due-date-exceptions/page.tsx

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { toaster } from "@/components/ui/toaster";
88
import { useClassProfiles } from "@/hooks/useClassProfiles";
99
import { useAssignmentDueDate, useCourse, useCourseController, useStudentRoster } from "@/hooks/useCourseController";
1010
import { useListTableControllerValues, useTableControllerValueById } from "@/lib/TableController";
11+
import { useVirtualizedRowWindow } from "@/hooks/useVirtualizedRowWindow";
1112
import { Assignment, AssignmentDueDateException, AssignmentGroup, UserProfile } from "@/utils/supabase/DatabaseTypes";
1213
import { Database } from "@/utils/supabase/SupabaseTypes";
1314
import {
@@ -608,16 +609,7 @@ export default function DueDateExceptions() {
608609
const course = useCourse();
609610
const { assignment_id } = useParams();
610611
const { assignments, assignmentGroupsWithMembers, assignmentDueDateExceptions } = useCourseController();
611-
//Ensure all data is fresh
612-
useEffect(() => {
613-
assignments.refetchAll();
614-
}, [assignments]);
615-
useEffect(() => {
616-
assignmentDueDateExceptions.refetchAll();
617-
}, [assignmentDueDateExceptions]);
618-
useEffect(() => {
619-
assignmentGroupsWithMembers.refetchAll();
620-
}, [assignmentGroupsWithMembers]);
612+
// TableControllers already hydrate/fetch and stay realtime; avoid forced mount refetches on large classes.
621613
const controller = useCourseController();
622614

623615
// Get assignment data
@@ -815,6 +807,11 @@ export default function DueDateExceptions() {
815807
}
816808
}
817809
});
810+
const tableRows = table.getRowModel().rows;
811+
const rowWindow = useVirtualizedRowWindow(tableRows, {
812+
estimatedRowHeight: 68,
813+
minRowsForVirtualization: 60
814+
});
818815

819816
if (!assignment) {
820817
return <Skeleton height="400px" width="100%" />;
@@ -849,59 +846,71 @@ export default function DueDateExceptions() {
849846
</Box>
850847

851848
{/* Table */}
852-
<Box w="100%" overflowX="auto" maxW="100vw" maxH="100vh" overflowY="auto">
853-
<Table.Root minW="0" w="100%">
854-
<Table.Header>
855-
{table.getHeaderGroups().map((headerGroup) => (
856-
<Table.Row key={headerGroup.id}>
857-
{headerGroup.headers.map((header) => (
858-
<Table.ColumnHeader
859-
key={header.id}
860-
bg="bg.muted"
861-
style={{
862-
position: "sticky",
863-
top: 0,
864-
zIndex: 20
865-
}}
866-
>
867-
{header.isPlaceholder ? null : (
868-
<Text onClick={header.column.getToggleSortingHandler()}>
869-
{flexRender(header.column.columnDef.header, header.getContext())}
870-
{{
871-
asc: (
849+
<Box w="100%" overflowX="auto" maxW="100vw">
850+
<Box ref={rowWindow.containerRef} onScroll={rowWindow.onScroll} overflowY="auto" maxH="70vh">
851+
<Table.Root minW="0" w="100%">
852+
<Table.Header>
853+
{table.getHeaderGroups().map((headerGroup) => (
854+
<Table.Row key={headerGroup.id}>
855+
{headerGroup.headers.map((header) => (
856+
<Table.ColumnHeader
857+
key={header.id}
858+
bg="bg.muted"
859+
style={{
860+
position: "sticky",
861+
top: 0,
862+
zIndex: 20
863+
}}
864+
>
865+
{header.isPlaceholder ? null : (
866+
<Text onClick={header.column.getToggleSortingHandler()}>
867+
{flexRender(header.column.columnDef.header, header.getContext())}
868+
{{
869+
asc: (
870+
<Icon size="md">
871+
<FaSortUp />
872+
</Icon>
873+
),
874+
desc: (
875+
<Icon size="md">
876+
<FaSortDown />
877+
</Icon>
878+
)
879+
}[header.column.getIsSorted() as string] ?? (
872880
<Icon size="md">
873-
<FaSortUp />
881+
<FaSort />
874882
</Icon>
875-
),
876-
desc: (
877-
<Icon size="md">
878-
<FaSortDown />
879-
</Icon>
880-
)
881-
}[header.column.getIsSorted() as string] ?? (
882-
<Icon size="md">
883-
<FaSort />
884-
</Icon>
885-
)}
886-
</Text>
887-
)}
888-
</Table.ColumnHeader>
889-
))}
890-
</Table.Row>
891-
))}
892-
</Table.Header>
893-
<Table.Body>
894-
{table.getRowModel().rows.map((row, idx) => (
895-
<Table.Row key={row.id} bg={idx % 2 === 0 ? "bg.subtle" : undefined}>
896-
{row.getVisibleCells().map((cell) => (
897-
<Table.Cell key={cell.id} p={2}>
898-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
899-
</Table.Cell>
900-
))}
901-
</Table.Row>
902-
))}
903-
</Table.Body>
904-
</Table.Root>
883+
)}
884+
</Text>
885+
)}
886+
</Table.ColumnHeader>
887+
))}
888+
</Table.Row>
889+
))}
890+
</Table.Header>
891+
<Table.Body>
892+
{rowWindow.shouldVirtualize && rowWindow.paddingTop > 0 ? (
893+
<Table.Row>
894+
<Table.Cell colSpan={columns.length} p={0} border="none" h={`${rowWindow.paddingTop}px`} />
895+
</Table.Row>
896+
) : null}
897+
{rowWindow.visibleRows.map((row) => (
898+
<Table.Row key={row.id} bg={row.index % 2 === 0 ? "bg.subtle" : undefined}>
899+
{row.getVisibleCells().map((cell) => (
900+
<Table.Cell key={cell.id} p={2}>
901+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
902+
</Table.Cell>
903+
))}
904+
</Table.Row>
905+
))}
906+
{rowWindow.shouldVirtualize && rowWindow.paddingBottom > 0 ? (
907+
<Table.Row>
908+
<Table.Cell colSpan={columns.length} p={0} border="none" h={`${rowWindow.paddingBottom}px`} />
909+
</Table.Row>
910+
) : null}
911+
</Table.Body>
912+
</Table.Root>
913+
</Box>
905914
</Box>
906915

907916
<Text>{studentData.length} Students</Text>

app/course/[course_id]/manage/assignments/[assignment_id]/layout.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AssignmentProvider } from "@/hooks/useAssignment";
2-
import { createClientWithCaching, fetchAssignmentControllerData, getUserRolesForCourse } from "@/lib/ssrUtils";
2+
import { createClientWithCaching, getUserRolesForCourse } from "@/lib/ssrUtils";
33
import { headers } from "next/headers";
44
import { redirect } from "next/navigation";
55
import { ManageAssignmentNav } from "./ManageAssignmentNav";
@@ -46,11 +46,9 @@ export default async function ManageAssignmentLayout({
4646
redirect(`/course/${courseId}`);
4747
}
4848

49-
// Pre-fetch all assignment controller data on the server with caching
50-
const initialData = await fetchAssignmentControllerData(
51-
assignmentId,
52-
role.role === "instructor" || role.role === "grader"
53-
);
49+
// Staff assignment pages should stream fast even with large courses and many submissions.
50+
// Let table controllers load on-demand on the client instead of blocking layout render.
51+
const initialData = undefined;
5452

5553
// Fetch assignment metadata for the title
5654
const client = await createClientWithCaching({ tags: ["assignment_metadata"] });

0 commit comments

Comments
 (0)