Render diff rows as divs to speed up big commit pages

A commit with 225 files and 43k changed lines produced 16 MB of HTML
with about 500k elements. The backend answered in 200 ms, the browser
needed 15 to 19 s to parse and lay it out.

Each diff row was a table row with four cells. It is now one div with
two spans. Line numbers live in data-old and data-new attributes and
the +/- sign comes from CSS generated content. The gutter width follows
the widest line number of the file via a CSS variable. The sign has a
whole-character width so tabs and spaces still align. Diff files also
get content-visibility: auto so off-screen files skip layout and paint.

Element count drops by about a fifth and the HTML shrinks by 2.4 MB.
Copy and paste stays free of line numbers and signs in Chrome and
Firefox.
AuthorKonata <konata@posteo.jp>
Date
Commite1f72fa1a19be126816c09b5d0e5074f74811e68
Parent72e42c5
4 files changed, 68 insertions(+), 49 deletions(-)
▾Minternal/web/e2e/patches_test.go
@@ -235,8 +235,8 @@ new file mode 100644
t.Run("patch diff is displayed with highlighted table", func(t *testing.T) {
r := admin.get(cleanURL + "?tab=changes")
if !r.Has(".diff-table") {
t.Error("diff-table missing")
if !r.Has(".diff-rows") {
t.Error("diff-rows missing")
}
if r.Count(".diff-row-add") == 0 {
t.Error("no added diff rows")
▾Minternal/web/e2e/repos_test.go
@@ -167,10 +167,10 @@ func TestRepos(t *testing.T) {
}
})
t.Run("commit detail shows diff table with added lines", func(t *testing.T) {
t.Run("commit detail shows diff with added lines", func(t *testing.T) {
r := admin.get(commitURL)
if !r.Has(".diff-table") {
t.Error("diff-table missing")
if !r.Has(".diff-rows") {
t.Error("diff-rows missing")
}
if n := r.Count(".diff-row-add"); n == 0 {
t.Error("no added rows")
@@ -180,9 +180,9 @@ func TestRepos(t *testing.T) {
}
})
t.Run("commit detail diff table has line numbers", func(t *testing.T) {
t.Run("commit detail diff has line numbers", func(t *testing.T) {
r := admin.get(commitURL)
if got := r.Text(".diff-row-add .diff-ln-new"); got != "1" {
if got := r.Attr(".diff-row-add .diff-ln", "data-new"); got != "1" {
t.Errorf("first new line number = %q", got)
}
})
▾Minternal/web/views/diffview.go
@@ -243,37 +243,38 @@ func diffBody(f highlight.RenderedDiffFile) g.Node {
if len(f.Hunks) == 0 {
return Div(Class("diff-binary-notice"), g.Text("No textual changes."))
}
return Div(Class("diff-file-body"),
// The line-number gutter is CSS generated content with a fixed width, so
// the width has to follow the widest line number of the file.
digits := 0
for _, h := range f.Hunks {
for _, r := range h.Rows {
digits = max(digits, len(strconv.Itoa(r.OldLine)), len(strconv.Itoa(r.NewLine)))
}
}
return Div(Class("diff-file-body"), Style("--ln-digits:"+strconv.Itoa(digits)),
g.Map(f.Hunks, func(hunk highlight.RenderedHunk) g.Node {
return Div(Class("diff-hunk"),
Table(Class("diff-table"),
THead(Tr(Th(ColSpan("4"), Class("diff-hunk-header"), g.Text(hunk.Header)))),
TBody(g.Map(hunk.Rows, diffRow)),
Div(Class("diff-rows"),
Div(Class("diff-hunk-header"), g.Text(hunk.Header)),
g.Map(hunk.Rows, diffRow),
),
)
}),
)
}
// diffRow keeps the element count low on purpose: big diffs are bound by HTML
// parsing, so line numbers and the +/- sign come from CSS, not from elements.
func diffRow(row highlight.RenderedRow) g.Node {
lineNo := func(n int) g.Node {
lineNo := func(name string, n int) g.Node {
if n == highlight.NoLine {
return nil
}
return g.Text(strconv.Itoa(n))
}
sign := " "
switch row.Type {
case "add":
sign = "+"
case "del":
sign = "-"
return Data(name, strconv.Itoa(n))
}
return Tr(Class("diff-row diff-row-"+row.Type),
Td(Class("diff-ln diff-ln-old"), lineNo(row.OldLine)),
Td(Class("diff-ln diff-ln-new"), lineNo(row.NewLine)),
Td(Class("diff-sign"), g.Text(sign)),
return Div(Class("diff-row diff-row-"+row.Type),
Span(Class("diff-ln"), lineNo("old", row.OldLine), lineNo("new", row.NewLine)),
// The highlighter already escaped this HTML.
Td(Class("diff-code"), g.Raw(row.HTML)),
Span(Class("diff-code"), g.Raw(row.HTML)),
)
}
▾Mweb/static/assets/css/code.css
@@ -498,6 +498,10 @@
margin-bottom: var(--space-4);
overflow: hidden;
min-width: 0;
/* Skip layout and paint of files that are off screen. Big diffs
otherwise block the first paint for many seconds. */
content-visibility: auto;
contain-intrinsic-size: auto 40rem;
}
.diff-file-header {
display: flex;
@@ -591,48 +595,60 @@
}
.diff-hunk-header {
padding: var(--space-1) var(--space-3);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: normal;
text-align: left;
background: color-mix(in srgb, var(--color-merged-bg) 50%, var(--color-bg));
color: var(--color-merged);
border-bottom: 1px solid var(--color-border-muted);
white-space: pre;
}
/* --- Diff table --- */
.diff-table {
/* --- Diff rows ---
One div per row, one span for both line numbers, one span for the code.
Line numbers and the sign are CSS generated content. Big diffs are bound
by HTML parsing, so fewer elements per row is what makes them fast. */
.diff-rows {
width: max-content;
min-width: 100%;
border-collapse: collapse;
font-family: var(--font-mono);
font-size: var(--text-xs);
line-height: 1.5;
tab-size: 4;
}
.diff-row {
display: flex;
}
.diff-ln {
width: 2.75rem;
min-width: 2.75rem;
display: flex;
flex: none;
color: var(--color-text-muted);
}
.diff-ln::before,
.diff-ln::after {
display: block;
width: calc(max(var(--ln-digits, 3), 3) * 1ch + 2 * var(--space-2));
text-align: right;
padding: 0 var(--space-2);
color: var(--color-text-muted);
border-right: 1px solid var(--color-border-muted);
user-select: none;
vertical-align: top;
white-space: nowrap;
}
.diff-sign {
width: 1.125rem;
min-width: 1.125rem;
text-align: center;
padding: 0 var(--space-1);
user-select: none;
vertical-align: top;
.diff-ln::before {
content: attr(data-old);
}
.diff-ln::after {
content: attr(data-new);
}
.diff-code {
padding: 0 var(--space-3);
flex: 1;
padding-right: var(--space-3);
white-space: pre;
vertical-align: top;
}
/* Whole character widths and no margin keep the tab grid of the code
aligned with a 4-space indent. */
.diff-code::before {
content: " ";
display: inline-block;
width: 4ch;
text-align: center;
}
.diff-row-add {
background: color-mix(in srgb, var(--color-success-bg) 70%, transparent);
@@ -641,18 +657,20 @@
background: color-mix(in srgb, var(--color-danger-bg) 70%, transparent);
}
.diff-row-add .diff-ln,
.diff-row-add .diff-sign {
.diff-row-add .diff-code::before {
background: color-mix(in srgb, var(--color-success-bg) 90%, transparent);
}
.diff-row-del .diff-ln,
.diff-row-del .diff-sign {
.diff-row-del .diff-code::before {
background: color-mix(in srgb, var(--color-danger-bg) 90%, transparent);
}
.diff-row-add .diff-sign {
.diff-row-add .diff-code::before {
content: "+";
color: var(--color-success);
font-weight: 700;
}
.diff-row-del .diff-sign {
.diff-row-del .diff-code::before {
content: "-";
color: var(--color-danger);
font-weight: 700;
}