Render off-screen diffs in a single pre tag to boost performance

ui/offscreen-diff-contents
“tan-nhu” 2024-06-14 17:40:21 -07:00
parent 263e8647aa
commit e6dc4c4fc9
6 changed files with 104 additions and 53 deletions

View File

@ -31,7 +31,7 @@ export default {
PULL_REQUEST_DIFF_RENDERING_BLOCK_SIZE: 10,
/** Detection margin for on-screen / off-screen rendering optimization. In pixels. */
IN_VIEWPORT_DETECTION_MARGIN: 5_000,
IN_VIEWPORT_DETECTION_MARGIN: 256_000,
/** Limit for the secret input in bytes */
SECRET_LIMIT_IN_BYTES: 5_242_880

View File

@ -363,8 +363,6 @@ const CommentsThread = <T = unknown,>({
let commentRow = annotatedRow.nextElementSibling as HTMLElement
while (commentRow?.dataset?.annotatedLine) {
toggleHidden(commentRow)
// Toggle opposite place-holder as well
const diffParent = commentRow.closest('.d2h-code-wrapper')?.parentElement
const oppositeDiv = diffParent?.classList.contains('right')
@ -376,6 +374,7 @@ const CommentsThread = <T = unknown,>({
oppositePlaceHolders?.forEach(dom => toggleHidden(dom))
toggleHidden(commentRow)
commentRow = commentRow.nextElementSibling as HTMLElement
}
show.current = !show.current
@ -388,6 +387,7 @@ const CommentsThread = <T = unknown,>({
button.classList.add(css.toggleComment)
button.title = getString('pr.toggleComments')
button.dataset.toggleComment = 'true'
button.addEventListener('keydown', e => {
if (e.key === 'Enter') toggleComments(e)

View File

@ -1 +1 @@
<svg fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" height="20" maskUnits="userSpaceOnUse" width="20" x="0" y="0"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#fff" fill-rule="evenodd"/></mask><mask id="b" fill="#fff"><path clip-rule="evenodd" d="m7.33325 7.91797c-.27614 0-.5.22386-.5.5s.22386.5.5.5h5.33335c.2761 0 .5-.22386.5-.5s-.2239-.5-.5-.5zm0 1.83333c-.27614 0-.5.22386-.5.5 0 .2761.22386.5.5.5h4.66665c.2762 0 .5-.2239.5-.5 0-.27614-.2238-.5-.5-.5z" fill="#fff" fill-rule="evenodd"/></mask><g opacity=".9"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#d8d8d8" fill-rule="evenodd"/><g mask="url(#a)"><path d="m0 0h20v20h-20z" fill="#ff661a"/><path d="m7.33341 5.33203h5.33329c1.4728 0 2.6667 1.19391 2.6667 2.66667v2.6667c0 1.4727-1.1939 2.6666-2.6667 2.6666h-2.6666l-2.66669 2v-2c-1.47275 0-2.66666-1.1939-2.66666-2.6666v-2.6667c0-1.47276 1.19391-2.66667 2.66666-2.66667z" stroke="#f3f3fa" stroke-linecap="round" stroke-linejoin="round"/><path d="m7.83325 8.41797c0 .27614-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .82843.67157 1.5 1.5 1.5zm5.33335 0h-5.33335v2h5.33335zm-.5.5c0-.27614.2238-.5.5-.5v2c.8284 0 1.5-.67157 1.5-1.5zm.5.5c-.2762 0-.5-.22386-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-5.33335 0h5.33335v-2h-5.33335zm.5 1.33333c0 .2761-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .8284.67157 1.5 1.5 1.5zm4.66665 0h-4.66665v2h4.66665zm-.5.5c0-.27614.2239-.5.5-.5v2c.8284 0 1.5-.6716 1.5-1.5zm.5.5c-.2761 0-.5-.2239-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-4.66665 0h4.66665v-2h-4.66665z" fill="#f3f3fa" mask="url(#b)" opacity=".5"/></g></g></svg>
<svg fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" height="20" maskUnits="userSpaceOnUse" width="20" x="0" y="0"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#fff" fill-rule="evenodd"/></mask><mask id="b" fill="#fff"><path clip-rule="evenodd" d="m7.33325 7.91797c-.27614 0-.5.22386-.5.5s.22386.5.5.5h5.33335c.2761 0 .5-.22386.5-.5s-.2239-.5-.5-.5zm0 1.83333c-.27614 0-.5.22386-.5.5 0 .2761.22386.5.5.5h4.66665c.2762 0 .5-.2239.5-.5 0-.27614-.2238-.5-.5-.5z" fill="#fff" fill-rule="evenodd"/></mask><g opacity=".9"><path clip-rule="evenodd" d="m10 20c5.5228 0 10-4.4772 10-10 0-5.52285-4.4772-10-10-10-5.52285 0-10 4.47715-10 10 0 5.5228 4.47715 10 10 10z" fill="#d8d8d8" fill-rule="evenodd"/><g mask="url(#a)"><path d="m0 0h20v20h-20z" fill="#1b841d"/><path d="m7.33341 5.33203h5.33329c1.4728 0 2.6667 1.19391 2.6667 2.66667v2.6667c0 1.4727-1.1939 2.6666-2.6667 2.6666h-2.6666l-2.66669 2v-2c-1.47275 0-2.66666-1.1939-2.66666-2.6666v-2.6667c0-1.47276 1.19391-2.66667 2.66666-2.66667z" stroke="#f3f3fa" stroke-linecap="round" stroke-linejoin="round"/><path d="m7.83325 8.41797c0 .27614-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .82843.67157 1.5 1.5 1.5zm5.33335 0h-5.33335v2h5.33335zm-.5.5c0-.27614.2238-.5.5-.5v2c.8284 0 1.5-.67157 1.5-1.5zm.5.5c-.2762 0-.5-.22386-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-5.33335 0h5.33335v-2h-5.33335zm.5 1.33333c0 .2761-.22386.5-.5.5v-2c-.82843 0-1.5.67157-1.5 1.5zm-.5-.5c.27614 0 .5.22386.5.5h-2c0 .8284.67157 1.5 1.5 1.5zm4.66665 0h-4.66665v2h4.66665zm-.5.5c0-.27614.2239-.5.5-.5v2c.8284 0 1.5-.6716 1.5-1.5zm.5.5c-.2761 0-.5-.2239-.5-.5h2c0-.82843-.6716-1.5-1.5-1.5zm-4.66665 0h4.66665v-2h-4.66665z" fill="#f3f3fa" mask="url(#b)" opacity=".5"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -465,14 +465,12 @@
border-bottom-right-radius: 4px;
max-width: calc(var(--page-container-width) - 48px);
&.hidden {
height: var(--block-height);
content-visibility: auto;
contain-intrinsic-size: auto var(--line-height);
* {
display: none !important;
}
.offscreenText {
font-size: 12px;
white-space: normal;
line-height: 20px;
margin: 0;
color: transparent;
}
}
}

View File

@ -26,8 +26,8 @@ export declare const expandCollapseDiffBtn: string
export declare const fileChanged: string
export declare const fname: string
export declare const fnamePopover: string
export declare const hidden: string
export declare const main: string
export declare const offscreenText: string
export declare const popover: string
export declare const readOnly: string
export declare const selectoSelection: string

View File

@ -46,10 +46,10 @@ import { CopyButton } from 'components/CopyButton/CopyButton'
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
import type { UseGetPullRequestInfoResult } from 'pages/PullRequest/useGetPullRequestInfo'
import { useQueryParams } from 'hooks/useQueryParams'
import { useCustomEventListener } from 'hooks/useEventListener'
import { useCustomEventListener, useEventListener } from 'hooks/useEventListener'
import { useShowRequestError } from 'hooks/useShowRequestError'
import { getErrorMessage, isInViewport } from 'utils/Utils'
import { createRequestIdleCallbackTaskPool } from 'utils/Task'
import { createRequestAnimationFrameTaskPool } from 'utils/Task'
import { useResizeObserver } from 'hooks/useResizeObserver'
import { useFindGitBranch } from 'hooks/useFindGitBranch'
import Config from 'Config'
@ -165,6 +165,7 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
},
[ref]
)
const contentHTML = useRef<string | null>(null)
useResizeObserver(
contentRef,
@ -178,20 +179,6 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
)
)
useEffect(() => {
let taskId = 0
if (inView) {
taskId = scheduleLowPriorityTask(() => {
if (isMounted.current && contentRef.current) contentRef.current.classList.remove(css.hidden)
})
} else {
taskId = scheduleLowPriorityTask(() => {
if (isMounted.current && contentRef.current) contentRef.current.classList.add(css.hidden)
})
}
return () => cancelTask(taskId)
}, [inView, isMounted])
//
// Handling custom events sent to DiffViewer from external components/features
// such as "jump to file", "jump to comment", etc...
@ -290,7 +277,7 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
if (isInViewport(containerRef.current as Element, 1000)) {
renderDiffAndComments()
} else {
taskId = scheduleLowPriorityTask(renderDiffAndComments)
taskId = scheduleTask(renderDiffAndComments)
}
}
@ -316,6 +303,67 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
const branchInfo = useFindGitBranch(pullReqMetadata?.source_branch)
useEffect(
function serializeDeserializeContent() {
const dom = contentRef.current
if (inView) {
if (isMounted.current && dom && contentHTML.current) {
dom.innerHTML = contentHTML.current
contentHTML.current = null
// Remove all signs from the raw HTML that CommentBox was mounted so
// it can be mounted/re-rendered again freshly
dom.querySelectorAll('tr[data-source-line-number]').forEach(row => {
row.removeAttribute('data-source-line-number')
row.removeAttribute('data-comment-ids')
row.querySelector('button[data-toggle-comment="true"]')?.remove?.()
})
dom.querySelectorAll('tr[data-annotated-line],tr[data-place-holder-for-line]').forEach(row => {
row.remove?.()
})
// Attach comments again
commentsHook.current.attachAllCommentThreads()
}
} else {
if (isMounted.current && dom && !contentHTML.current) {
const { clientHeight, textContent, innerHTML } = dom
// Detach comments since they are no longer in sync in DOM as
// all DOMs are removed
commentsHook.current.detachAllCommentThreads()
// Save current innerHTML
contentHTML.current = innerHTML
// TODO: Might be good to clean textContent a bit to not include
// diff header info, line numbers, hunk headers, etc...
// Set innerHTML to a pre tag with the same height to avoid reflow
// The pre textContent allows Cmd/Ctrl-F to work
dom.innerHTML = `<pre style="height: ${clientHeight + 'px'}" class="${
css.offscreenText
}">${textContent}</pre>`
}
}
},
[inView, isMounted, commentsHook]
)
// Add click event listener from contentRef to handle click event on "Show Diff" button
// This can't be done from the button itself because it got serialized / deserialized from
// text during off-screen optimization (handler will be gone/destroyed)
useEventListener(
'click',
useCallback(function showDiff(event) {
if (((event.target as HTMLElement)?.closest('button') as HTMLElement)?.dataset?.action === ACTION_SHOW_DIFF) {
setRenderCustomContent(false)
}
}, []),
contentRef.current as HTMLDivElement
)
useShowRequestError(fullDiffError, 0)
useEffect(
@ -505,28 +553,32 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
</Container>
<Container id={diff.contentId} data-path={diff.filePath} className={css.diffContent} ref={contentRef}>
<Render when={renderCustomContent && !collapsed}>
<Container height={200} flex={{ align: 'center-center' }}>
<Layout.Vertical padding="xlarge" style={{ alignItems: 'center' }}>
<Render when={fileDeleted || isDiffTooLarge || diffHasVeryLongLine}>
<Button variation={ButtonVariation.LINK} onClick={() => setRenderCustomContent(false)}>
{getString('pr.showDiff')}
</Button>
</Render>
<Text>
{getString(
fileDeleted
? 'pr.fileDeleted'
: isDiffTooLarge || diffHasVeryLongLine
? 'pr.diffTooLarge'
: isBinary
? 'pr.fileBinary'
: 'pr.fileUnchanged'
)}
</Text>
</Layout.Vertical>
</Container>
</Render>
{/* Note: This parent container is needed to make sure "Show Diff" work correctly
with content converted between textContent and innerHTML */}
<Container>
<Render when={renderCustomContent && !collapsed}>
<Container height={200} flex={{ align: 'center-center' }}>
<Layout.Vertical padding="xlarge" style={{ alignItems: 'center' }}>
<Render when={fileDeleted || isDiffTooLarge || diffHasVeryLongLine}>
<Button variation={ButtonVariation.LINK} onClick={() => setRenderCustomContent(false)}>
{getString('pr.showDiff')}
</Button>
</Render>
<Text>
{getString(
fileDeleted
? 'pr.fileDeleted'
: isDiffTooLarge || diffHasVeryLongLine
? 'pr.diffTooLarge'
: isBinary
? 'pr.fileBinary'
: 'pr.fileUnchanged'
)}
</Text>
</Layout.Vertical>
</Container>
</Render>
</Container>
</Container>
</Layout.Vertical>
<NavigationCheck when={dirty} />
@ -535,6 +587,7 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
}
const BLOCK_HEIGHT = '--block-height'
const ACTION_SHOW_DIFF = 'showDiff'
export enum DiffViewerEvent {
SCROLL_INTO_VIEW = 'scrollIntoView'
@ -551,6 +604,6 @@ export interface DiffViewerExchangeState {
fullDiff?: DiffFileEntry
}
const { scheduleTask: scheduleLowPriorityTask, cancelTask } = createRequestIdleCallbackTaskPool()
const { scheduleTask, cancelTask } = createRequestAnimationFrameTaskPool()
export const DiffViewer = React.memo(DiffViewerInternal)