Repository: gradio-app/gradio
Pull request: significantly improve the performance of `gr.Dataframe` for large datasets
## Description

Creating PR to do some more testing but I think this is working.

You can test with the following code:

```python
import gradio as gr

df = [[f"col: {i} -- row:{j}" for j in range(1, 11)] for i in range(1, 10000)]

with gr.Blocks() as demo:
    gr.DataFrame(df, headers=list(range(1, 11)), interactive=True)

if __name__ == "__main__":
    demo.launch()
```

Closes: #5343

## 🎯 PRs Should Target Issues

Before your create a PR, please check to see if there is [an existing issue](https://github.com/gradio-app/gradio/issues) for this change. If not, please create an issue before you create this PR, unless the fix is very small. 

Not adhering to this guideline will result in the PR being closed. 

## Tests

1. PRs will only be merged if tests pass on CI. To run the tests locally, please set up [your Gradio environment locally](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md) and run the tests: `bash scripts/run_all_tests.sh`

2. You may need to run the linters: `bash scripts/format_backend.sh` and `bash scripts/format_frontend.sh`
  


Problem statement:
datafrme is slow, make it go brrr
- [x] I have searched to see if a similar issue already exists.

This is separate from pagination / max_rows, although it could _replace_ pagination.

If you have a large dataset then the performance can get pretty bad with the current dataframe.

Hints from issue discussion:
Motion to rename the issue to "datafrme is slow, make it go brr"

Commit message: add changeset

Patch to review:
diff --git a/.changeset/five-gifts-bathe.md b/.changeset/five-gifts-bathe.md
new file mode 100644
index 0000000000..aead67b0a9
--- /dev/null
+++ b/.changeset/five-gifts-bathe.md
@@ -0,0 +1,8 @@
+---
+"@gradio/dataframe": minor
+"@gradio/markdown": minor
+"@gradio/theme": minor
+"gradio": minor
+---
+
+feat:significantly improve the performance of `gr.Dataframe` for large datasets
diff --git a/js/dataframe/shared/Table.svelte b/js/dataframe/shared/Table.svelte
index 0a6050ef90..39e28040bc 100644
--- a/js/dataframe/shared/Table.svelte
+++ b/js/dataframe/shared/Table.svelte
@@ -8,6 +8,7 @@
 	import EditableCell from "./EditableCell.svelte";
 	import type { SelectData } from "@gradio/utils";
 	import { _ } from "svelte-i18n";
+	import VirtualTable from "./VirtualTable.svelte";
 
 	type Datatype = "str" | "markdown" | "html" | "number" | "bool" | "date";
 
@@ -27,7 +28,7 @@
 
 	export let editable = true;
 	export let wrap = false;
-	export let height: number | undefined = undefined;
+	export let height: number | undefined;
 
 	let selected: false | string = false;
 
@@ -163,7 +164,7 @@
 
 	function get_sort_status(
 		name: string,
-		_sort: number,
+		_sort?: number,
 		direction?: SortDirection
 	): "none" | "ascending" | "descending" {
 		if (!_sort) return "none";
@@ -338,16 +339,8 @@
 	$: set_focus(selected, "select");
 
 	type SortDirection = "asc" | "des";
-	let sort_direction: SortDirection;
-	let sort_by: number;
-
-	function sort(col: number, dir: SortDirection): void {
-		if (dir === "asc") {
-			data = data.sort((a, b) => (a[col].value < b[col].value ? -1 : 1));
-		} else if (dir === "des") {
-			data = data.sort((a, b) => (a[col].value > b[col].value ? -1 : 1));
-		}
-	}
+	let sort_direction: SortDirection | undefined;
+	let sort_by: number | undefined;
 
 	function handle_sort(col: number): void {
 		if (typeof sort_by !== "number" || sort_by !== col) {
@@ -361,7 +354,7 @@
 			}
 		}
 
-		sort(col, sort_direction);
+		// sort(col, sort_direction);
 	}
 
 	let header_edit: string | false;
@@ -527,6 +520,43 @@
 	}
 
 	let dragging = false;
+
+	let t_width = 0;
+
+	function get_max(
+		_d: { value: any; id: string }[][]
+	): { value: any; id: string }[] {
+		let max = _d[0].slice();
+		let min = _d[0].slice();
+		for (let i = 0; i < _d.length; i++) {
+			for (let j = 0; j < _d[i].length; j++) {
+				if (`${max[j].value}`.length < `${_d[i][j].value}`.length) {
+					max[j] = _d[i][j];
+				}
+			}
+		}
+
+		return max;
+	}
+
+	$: max = get_max(data);
+
+	$: cells[0] && set_cell_widths();
+	let cells: HTMLTableCellElement[] = [];
+	let parent: HTMLDivElement;
+
+	function set_cell_widths(): void {
+		const widths = cells.map((el, i) => {
+			return el?.clientWidth || 0;
+		});
+
+		if (widths.length === 0) return;
+		for (let i = 0; i < widths.length; i++) {
+			parent.style.setProperty(`--cell-width-${i}`, `${widths[i]}px`);
+		}
+	}
+
+	let table_height: number = height || 500;
 </script>
 
 <svelte:window
@@ -541,11 +571,70 @@
 		</p>
 	{/if}
 	<div
+		bind:this={parent}
 		class="table-wrap"
 		class:dragging
 		class:no-wrap={!wrap}
-		style="max-height: {typeof height === undefined ? 'auto' : height + 'px'};"
+		style="height:{table_height}px"
 	>
+		<table bind:clientWidth={t_width}>
+			{#if label && label.length !== 0}
+				<caption class="sr-only">{label}</caption>
+			{/if}
+			<thead>
+				<tr>
+					{#each _headers as { value, id }, i (id)}
+						<th
+							class:editing={header_edit === id}
+							aria-sort={get_sort_status(value, sort_by, sort_direction)}
+						>
+							<div class="cell-wrap">
+								<EditableCell
+									{value}
+									{latex_delimiters}
+									header
+									edit={false}
+									el={null}
+								/>
+
+								<div
+									class:sorted={sort_by === i}
+									class:des={sort_by === i && sort_direction === "des"}
+									class="sort-button {sort_direction} "
+								>
+									<svg
+										width="1em"
+										height="1em"
+										viewBox="0 0 9 7"
+										fill="none"
+										xmlns="http://www.w3.org/2000/svg"
+									>
+										<path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
+									</svg>
+								</div>
+							</div>
+						</th>
+					{/each}
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					{#each max as { value, id }, j (id)}
+						<td tabindex="0" bind:this={cells[j]}>
+							<div class:border-transparent={selected !== id} class="cell-wrap">
+								<EditableCell
+									{value}
+									{latex_delimiters}
+									datatype={Array.isArray(datatype) ? datatype[j] : datatype}
+									edit={false}
+									el={null}
+								/>
+							</div>
+						</td>
+					{/each}
+				</tr>
+			</tbody>
+		</table>
 		<Upload
 			flex={false}
 			center={false}
@@ -554,86 +643,83 @@
 			on:load={(e) => blob_to_string(data_uri_to_blob(e.detail.data))}
 			bind:dragging
 		>
-			<table class:dragging>
+			<VirtualTable
+				items={data}
+				sort={[sort_by, sort_direction]}
+				table_width={t_width}
+				max_height={height || 300}
+				bind:actual_height={table_height}
+			>
 				{#if label && label.length !== 0}
 					<caption class="sr-only">{label}</caption>
 				{/if}
-				<thead>
-					<tr>
-						{#each _headers as { value, id }, i (id)}
-							<th
-								bind:this={els[id].cell}
-								class:editing={header_edit === id}
-								aria-sort={get_sort_status(value, sort_by, sort_direction)}
-							>
-								<div class="cell-wrap">
-									<EditableCell
-										{value}
-										{latex_delimiters}
-										bind:el={els[id].input}
-										edit={header_edit === id}
-										on:keydown={end_header_edit}
-										on:dblclick={() => edit_header(id)}
-										header
-									/>
-
-									<!-- TODO: fix -->
-									<!-- svelte-ignore a11y-click-events-have-key-events -->
-									<!-- svelte-ignore a11y-no-static-element-interactions-->
-									<div
-										class:sorted={sort_by === i}
-										class:des={sort_by === i && sort_direction === "des"}
-										class="sort-button {sort_direction} "
-										on:click={() => handle_sort(i)}
-									>
-										<svg
-											width="1em"
-											height="1em"
-											viewBox="0 0 9 7"
-											fill="none"
-											xmlns="http://www.w3.org/2000/svg"
-										>
-											<path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
-										</svg>
-									</div>
-								</div>
-							</th>
-						{/each}
-					</tr>
-				</thead>
-
-				<tbody>
-					{#each data as row, i (row)}
-						<tr>
-							{#each row as { value, id }, j (id)}
-								<td
-									tabindex="0"
-									bind:this={els[id].cell}
-									on:touchstart={() => start_edit(id)}
-									on:click={() => handle_cell_click(id)}
-									on:dblclick={() => start_edit(id)}
-									on:keydown={(e) => handle_keydown(e, i, j, id)}
+				<tr slot="thead">
+					{#each _headers as { value, id }, i (id)}
+						<th
+							bind:this={els[id].cell}
+							class:editing={header_edit === id}
+							aria-sort={get_sort_status(value, sort_by, sort_direction)}
+							style="width: var(--cell-width-{i});"
+						>
+							<div class="cell-wrap">
+								<EditableCell
+									bind:value={_headers[i].value}
+									bind:el={els[id].input}
+									{latex_delimiters}
+									edit={header_edit === id}
+									on:keydown={end_header_edit}
+									on:dblclick={() => edit_header(id)}
+									header
+								/>
+
+								<!-- TODO: fix -->
+								<!-- svelte-ignore a11y-click-events-have-key-events -->
+								<!-- svelte-ignore a11y-no-static-element-interactions-->
+								<div
+									class:sorted={sort_by === i}
+									class:des={sort_by === i && sort_direction === "des"}
+									class="sort-button {sort_direction} "
+									on:click={() => handle_sort(i)}
 								>
-									<div
-										class:border-transparent={selected !== id}
-										class="cell-wrap"
+									<svg
+										width="1em"
+										height="1em"
+										viewBox="0 0 9 7"
+										fill="none"
+										xmlns="http://www.w3.org/2000/svg"
 									>
-										<EditableCell
-											bind:value
-											bind:el={els[id].input}
-											{latex_delimiters}
-											edit={editing === id}
-											datatype={Array.isArray(datatype)
-												? datatype[j]
-												: datatype}
-										/>
-									</div>
-								</td>
-							{/each}
-						</tr>
+										<path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
+									</svg>
+								</div>
+							</div>
+						</th>
+					{/each}
+				</tr>
+
+				<tr slot="tbody" let:item let:index class:row_odd={index % 2 === 0}>
+					{#each item as { value, id }, j (id)}
+						<td
+							tabindex="0"
+							bind:this={els[id].cell}
+							on:touchstart={() => start_edit(id)}
+							on:click={() => handle_cell_click(id)}
+							on:dblclick={() => start_edit(id)}
+							on:keydown={(e) => handle_keydown(e, index, j, id)}
+							style="width: var(--cell-width-{j});"
+						>
+							<div class:border-transparent={selected !== id} class="cell-wrap">
+								<EditableCell
+									bind:value={data[index][j].value}
+									bind:el={els[id].input}
+									{latex_delimiters}
+									edit={editing === id}
+									datatype={Array.isArray(datatype) ? datatype[j] : datatype}
+								/>
+							</div>
+						</td>
 					{/each}
-				</tbody>
-			</table>
+				</tr>
+			</VirtualTable>
 		</Upload>
 	</div>
 	{#if editable}
@@ -710,7 +796,7 @@
 		border: 1px solid var(--border-color-primary);
 		border-radius: var(--table-radius);
 		overflow-x: auto;
-		overflow-y: auto;
+		overflow-y: hidden;
 	}
 
 	.dragging {
@@ -722,19 +808,22 @@
 	}
 
 	table {
+		position: absolute;
+		opacity: 0;
 		transition: 150ms;
 		width: var(--size-full);
 		table-layout: auto;
-		overflow: hidden;
+		/* overflow: hidden; */
 		color: var(--body-text-color);
 		font-size: var(--input-text-size);
 		line-height: var(--line-md);
 		font-family: var(--font-mono);
+		border-spacing: 0;
 	}
 
-	table.dragging {
+	/* table.dragging {
 		opacity: 0.4;
-	}
+	} */
 
 	thead {
 		position: sticky;
@@ -819,26 +908,6 @@
 		color: var(--color-accent);
 	}
 
-	tbody {
-		overflow-y: scroll;
-	}
-
-	tbody > tr:last-child {
-		border: none;
-	}
-
-	tbody > tr:nth-child(even) {
-		background: var(--table-even-background-fill);
-	}
-
-	tbody > tr:nth-child(odd) {
-		background: var(--table-odd-background-fill);
-	}
-
-	tbody > tr:nth-child(odd):focus {
-		background: var(--background-fill-primary);
-	}
-
 	.editing {
 		background: var(--table-editing);
 	}
@@ -860,4 +929,12 @@
 	.controls-wrap > * + * {
 		margin-left: var(--size-1);
 	}
+
+	.row_odd {
+		background: var(--table-odd-background-fill);
+	}
+
+	.row_odd:focus {
+		background: var(--background-fill-primary);
+	}
 </style>
diff --git a/js/dataframe/shared/VirtualTable.svelte b/js/dataframe/shared/VirtualTable.svelte
new file mode 100644
index 0000000000..7ec293684c
--- /dev/null
+++ b/js/dataframe/shared/VirtualTable.svelte
@@ -0,0 +1,311 @@
+<script lang="ts">
+	import { onMount, tick } from "svelte";
+	import { _ } from "svelte-i18n";
+	type SortDirection = "asc" | "des";
+
+	export let items: any[][] = [];
+
+	export let sort: [number | undefined, SortDirection | undefined] = [
+		undefined,
+		undefined
+	];
+	export let table_width: number;
+	export let max_height: number;
+	export let actual_height;
+	export let start = 0;
+	export let end = 0;
+
+	let height = "100%";
+
+	let average_height: number;
+	let bottom = 0;
+	let contents: HTMLTableSectionElement;
+	let head_height = 0;
+	let foot_height = 0;
+	let height_map: number[] = [];
+	let mounted: boolean;
+	let rows: HTMLCollectionOf<HTMLTableRowElement>;
+	let top = 0;
+	let viewport: HTMLTableElement;
+	let viewport_height = 0;
+	let visible: { index: number; data: any[] }[] = [];
+
+	$: if (mounted)
+		requestAnimationFrame(() =>
+			refresh_height_map(sortedItems, viewport_height)
+		);
+
+	let content_height = 0;
+	async function refresh_height_map(
+		_items: typeof items,
+		viewport_height: number
+	): Promise<void> {
+		if (viewport_height === 0 || table_width === 0) {
+			return;
+		}
+		const { scrollTop } = viewport;
+		await tick();
+		content_height = top - (scrollTop - head_height);
+		let i = start;
+		while (content_height - 3 < max_height - head_height && i < _items.length) {
+			let row = rows[i - start];
+			if (!row) {
+				end = i + 1;
+				await tick(); // render the newly visible row
+				row = rows[i - start];
+			}
+			const row_height = (height_map[i] = row.getBoundingClientRect().height);
+			content_height += row_height;
+			i += 1;
+		}
+
+		end = i;
+		const remaining = _items.length - end;
+		average_height = (top + content_height) / end;
+		bottom = remaining * average_height + foot_height;
+		height_map.length = _items.length;
+		await scroll_to_index(0, { behavior: "auto" });
+	}
+
+	$: {
+		if (!max_height) {
+			actual_height = content_height + 3;
+		} else if (content_height < max_height) {
+			actual_height = content_height + 3;
+		} else {
+			actual_height = max_height;
+		}
+	}
+
+	function get_computed_px_amount(elem: HTMLElement, property: string): number {
+		const compStyle = getComputedStyle(elem);
+
+		let x = parseInt(compStyle.getPropertyValue(property));
+		return x;
+	}
+
+	async function handle_scroll(): Promise<void> {
+		rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;
+		const is_start_overflow = sortedItems.length < start;
+
+		const row_top_border = get_computed_px_amount(rows[1], "border-top-width");
+
+		const actual_border_collapsed_width = 0;
+
+		if (is_start_overflow) {
+			await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
+		}
+
+		const { scrollTop } = viewport;
+		let new_start = 0;
+		// acquire height map for currently visible rows
+		for (let v = 0; v < rows.length; v += 1) {
+			height_map[start + v] = rows[v].getBoundingClientRect().height;
+		}
+		let i = 0;
+		// start from top: thead, with its borders, plus the first border to afterwards neglect
+		let y = head_height + row_top_border / 2;
+		let row_heights = [];
+		// loop items to find new start
+		while (i < sortedItems.length) {
+			const row_height = height_map[i] || average_height;
+			row_heights[i] = row_height;
+			// we only want to jump if the full (incl. border) row is away
+			if (y + row_height + actual_border_collapsed_width > scrollTop) {
+				// this is the last index still inside the viewport
+				new_start = i;
+				top = y - (head_height + row_top_border / 2);
+				break;
+			}
+			y += row_height;
+			i += 1;
+		}
+
+		new_start = Math.max(0, new_start);
+		while (i < sortedItems.length) {
+			const row_height = height_map[i] || average_height;
+			y += row_height;
+			i += 1;
+			if (y > scrollTop + viewport_height) {
+				break;
+			}
+		}
+		start = new_start;
+		end = i;
+		const remaining = sortedItems.length - end;
+		if (end === 0) {
+			end = 10;
+		}
+		average_height = y / end;
+		let remaining_height = remaining * average_height; // 0
+		// compute height map for remaining items
+		while (i < sortedItems.length) {
+			i += 1;
+			height_map[i] = average_height;
+		}
+		bottom = remaining_height;
+		if (!isFinite(bottom)) {
+			bottom = 200000;
+		}
+	}
+
+	export async function scroll_to_index(
+		index: number,
+		opts: ScrollToOptions
+	): Promise<void> {
+		const { scrollTop } = viewport;
+		const itemsDelta = index - start;
+		const _itemHeight = average_height;
+		const distance = itemsDelta * _itemHeight;
+		const _opts = {
+			left: 0,
+			top: scrollTop + distance,
+			behavior: "smooth" as ScrollBehavior,
+			...opts
+		};
+		viewport.scrollTo(_opts);
+	}
+
+	$: sortedItems = sorted(items, sort[0], sort[1]);
+
+	$: visible = sortedItems.slice(start, end).map((data, i) => {
+		return { index: i + start, data };
+	});
+
+	function sort_data(
+		data: typeof items,
+		col?: number,
+		dir?: SortDirection
+	): typeof items {
+		if (!col || !dir) {
+			return data;
+		}
+		if (dir === "asc") {
+			return data.sort((a, b) => (a[col].value < b[col].value ? -1 : 1));
+		} else if (dir === "des") {
+			return data.sort((a, b) => (a[col].value > b[col].value ? -1 : 1));
+		}
+
+		return data;
+	}
+
+	function throttle(func: any, delay = 0): () => any {
+		let lastCall = 0;
+		return function (...args) {
+			const now = new Date().getTime();
+			if (now - lastCall < delay) {
+				return;
+			}
+			lastCall = now;
+			//@ts-ignore
+			return func.apply(this, args);
+		};
+	}
+
+	const throttle_scroll = throttle(handle_scroll, 16);
+
+	const sorted = (
+		_items: typeof items,
+		col?: number,
+		direction?: SortDirection
+	): typeof items => sort_data(_items, col, direction);
+
+	onMount(() => {
+		rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;
+		mounted = true;
+		refresh_height_map(items, viewport_height);
+	});
+</script>
+
+<svelte-virtual-table-viewport>
+	<table
+		class="table"
+		bind:this={viewport}
+		bind:offsetHeight={viewport_height}
+		on:scroll={throttle_scroll}
+		style="width:{table_width}px; height: {height}; --bw-svt-p-top: {top}px; --bw-svt-p-bottom: {bottom}px; --bw-svt-head-height: {head_height}px; --bw-svt-foot-height: {foot_height}px; --bw-svt-avg-row-height: {average_height}px"
+	>
+		<thead class="thead" bind:offsetHeight={head_height}>
+			<slot name="thead" />
+		</thead>
+		<tbody bind:this={contents} class="tbody">
+			{#each visible as item}
+				<slot name="tbody" item={item.data} index={item.index}>
+					Missing Table Row
+				</slot>
+			{/each}
+		</tbody>
+		<tfoot class="tfoot" bind:offsetHeight={foot_height}>
+			<slot name="tfoot" />
+		</tfoot>
+	</table>
+</svelte-virtual-table-viewport>
+
+<style type="text/css">
+	/* table {
+		transition: 150ms;
+		width: var(--size-full);
+		table-layout: auto;
+		overflow: hidden;
+	
+	} */
+	table {
+		/* opacity: 0; */
+		position: relative;
+		overflow-y: scroll;
+		overflow-x: hidden;
+		-webkit-overflow-scrolling: touch;
+		max-height: 100vh;
+		box-sizing: border-box;
+		display: block;
+		padding: 0;
+		margin: 0;
+		color: var(--body-text-color);
+		font-size: var(--input-text-size);
+		line-height: var(--line-md);
+		font-family: var(--font-mono);
+		/* table-layout: fixed; */
+		border-spacing: 0;
+	}
+	table :is(thead, tfoot, tbody) {
+		display: table;
+		table-layout: fixed;
+		width: 100%;
+		box-sizing: border-box;
+	}
+
+	table:not(.require-border-collapse) tbody {
+		padding-top: var(--bw-svt-p-top);
+		padding-bottom: var(--bw-svt-p-bottom);
+	}
+	tbody {
+		position: relative;
+		box-sizing: border-box;
+		border: 0px solid currentColor;
+	}
+
+	/** sortable styles */
+	thead :global(th.sortable) {
+		cursor: pointer;
+		user-select: none;
+		-moz-user-select: none;
+		-webkit-user-select: none;
+		-ms-user-select: none;
+	}
+
+	tbody > :global(tr:last-child) {
+		border: none;
+	}
+
+	tbody > :global(tr:nth-child(even)) {
+		background: var(--table-even-background-fill);
+	}
+
+	thead {
+		position: sticky;
+		top: 0;
+		left: 0;
+		z-index: var(--layer-1);
+		box-shadow: var(--shadow-drop);
+	}
+</style>
diff --git a/js/markdown/static/MarkdownCode.svelte b/js/markdown/static/MarkdownCode.svelte
index c68a2cbde0..63a6aa4b3d 100644
--- a/js/markdown/static/MarkdownCode.svelte
+++ b/js/markdown/static/MarkdownCode.svelte
@@ -24,7 +24,7 @@
 		left: string;
 		right: string;
 		display: boolean;
-	}[];
+	}[] = [];
 
 	let el: HTMLSpanElement;
 	let html: string;
diff --git a/js/theme/src/reset.css b/js/theme/src/reset.css
index 4c49454840..051c248ae7 100644
--- a/js/theme/src/reset.css
+++ b/js/theme/src/reset.css
@@ -79,7 +79,7 @@ sup {
 
 table {
 	border-color: inherit;
-	border-collapse: collapse;
+
 	text-indent: 0;
 }
 


Code review comments:
diff_hunk: @@ -722,19 +808,22 @@
 	}
 
 	table {
+		position: absolute;
+		opacity: 0;
 		transition: 150ms;
 		width: var(--size-full);
 		table-layout: auto;
-		overflow: hidden;
+		/* overflow: hidden; */
 		color: var(--body-text-color);
 		font-size: var(--input-text-size);
 		line-height: var(--line-md);
 		font-family: var(--font-mono);
+		border-spacing: 0;
 	}
 
-	table.dragging {
+	/* table.dragging {
path: js/dataframe/shared/Table.svelte
text: @user1:
Remove comment

diff_hunk: @@ -361,7 +354,7 @@
 			}
 		}
 
-		sort(col, sort_direction);
+		// sort(col, sort_direction);
path: js/dataframe/shared/Table.svelte
text: @user1:
Remove comment
```suggestion
```