fix: buffer large pastes outside the textarea to prevent UI freeze
Pasting (or drag-loading, or file-uploading) several MB of text into the <textarea> would lock up the browser for seconds while it laid out millions of characters. The bottleneck is the textarea itself, not the filter pipeline (filters only run on Save). Hold above-threshold (256 KB) content in a JS-side `bufferedContent` variable and render only the first 50 lines + a banner into the textarea. The Save path uses `bufferedContent` when set, so the upload sees the full content. The textarea becomes read-only while the buffer is active; user-typed input invalidates the buffer automatically (the textarea is the source of truth in normal mode). A new `loadContent(text)` helper is the single entry point: it chooses textarea vs. buffer based on length. All call sites (clipboard read, paste-event for text, file load, text drop) route through it. Threshold is 256 KB (above which textarea rendering visibly stutters); preview is 50 lines (enough to recognise the log without choking the renderer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,27 @@ const fileSelectButton = document.getElementById('paste-select-file');
|
||||
const pasteClipboardButton = document.getElementById('paste-clipboard');
|
||||
const pasteError = document.getElementById('paste-error');
|
||||
|
||||
/*
|
||||
* Large-paste buffering. <textarea> rendering is the bottleneck once content
|
||||
* crosses a few hundred KB; the browser locks up while it lays out millions
|
||||
* of characters. For pastes / file loads above the threshold, we hold the
|
||||
* full content in `bufferedContent` and only render a small preview into the
|
||||
* textarea. The save path uses `bufferedContent` when set.
|
||||
*/
|
||||
const PASTE_BUFFER_THRESHOLD_BYTES = 256 * 1024; // 256 KB
|
||||
const PASTE_PREVIEW_LINES = 50;
|
||||
let bufferedContent = null;
|
||||
|
||||
pasteArea.focus();
|
||||
pasteArea.addEventListener('input', reevaluateContentStatus);
|
||||
pasteArea.addEventListener('input', () => {
|
||||
// User-typed input invalidates the buffer (the textarea is now the
|
||||
// source of truth). The buffer is only set programmatically, so a
|
||||
// user-driven `input` event always means edit-from-preview.
|
||||
if (bufferedContent !== null) {
|
||||
clearBuffer();
|
||||
}
|
||||
reevaluateContentStatus();
|
||||
});
|
||||
pasteArea.addEventListener('paste', handlePasteEvent);
|
||||
pasteSaveButtons.forEach(button => button.addEventListener('click', sendLog));
|
||||
fileSelectButton.addEventListener('click', selectLogFile);
|
||||
@@ -39,7 +58,7 @@ async function sendLog() {
|
||||
pasteSaveButtons.forEach(button => button.classList.add("btn-working"));
|
||||
|
||||
try {
|
||||
let log = pasteArea.value;
|
||||
let log = bufferedContent ?? pasteArea.value;
|
||||
log = applyFilters(log);
|
||||
|
||||
const bodyData = {
|
||||
@@ -149,13 +168,56 @@ async function pasteFromClipboard() {
|
||||
showError("Clipboard is empty.");
|
||||
return;
|
||||
}
|
||||
pasteArea.value = content;
|
||||
reevaluateContentStatus();
|
||||
loadContent(content);
|
||||
} catch (err) {
|
||||
showError("Clipboard is empty or not accessible.");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Single entry point for "place this string in the editor."
|
||||
* Routes large content into the buffer + preview; small content goes into
|
||||
* the textarea normally so the user can keep editing it.
|
||||
*/
|
||||
function loadContent(text) {
|
||||
if (text.length > PASTE_BUFFER_THRESHOLD_BYTES) {
|
||||
loadIntoBuffer(text);
|
||||
} else {
|
||||
clearBuffer();
|
||||
pasteArea.value = text;
|
||||
reevaluateContentStatus();
|
||||
}
|
||||
}
|
||||
|
||||
function loadIntoBuffer(text) {
|
||||
bufferedContent = text;
|
||||
const lines = text.split('\n');
|
||||
const sizeMb = (text.length / 1024 / 1024).toFixed(2);
|
||||
if (lines.length <= PASTE_PREVIEW_LINES) {
|
||||
pasteArea.value = text;
|
||||
} else {
|
||||
const preview = lines.slice(0, PASTE_PREVIEW_LINES).join('\n');
|
||||
const remaining = lines.length - PASTE_PREVIEW_LINES;
|
||||
pasteArea.value =
|
||||
`[Large paste buffered: ${lines.length.toLocaleString()} lines, ${sizeMb} MB.\n` +
|
||||
` Full content uploads on Save. Edit this textarea to clear the buffer.]\n` +
|
||||
`\n` +
|
||||
`--- preview: first ${PASTE_PREVIEW_LINES} of ${lines.length.toLocaleString()} lines ---\n` +
|
||||
preview +
|
||||
`\n--- ${remaining.toLocaleString()} more lines hidden ---\n`;
|
||||
}
|
||||
pasteArea.readOnly = true;
|
||||
reevaluateContentStatus();
|
||||
}
|
||||
|
||||
function clearBuffer() {
|
||||
if (bufferedContent === null) {
|
||||
return;
|
||||
}
|
||||
bufferedContent = null;
|
||||
pasteArea.readOnly = false;
|
||||
}
|
||||
|
||||
function reevaluateContentStatus() {
|
||||
clearError();
|
||||
if (pasteArea.value.length > 0) {
|
||||
@@ -184,6 +246,14 @@ async function handlePasteEvent(e) {
|
||||
if (e.clipboardData?.files?.length > 0) {
|
||||
e.preventDefault();
|
||||
await loadFileContents(e.clipboardData.files[0]);
|
||||
return;
|
||||
}
|
||||
// Intercept large text pastes before the browser commits them to the
|
||||
// textarea — assigning multi-MB strings to a <textarea> freezes the page.
|
||||
const text = e.clipboardData?.getData('text');
|
||||
if (text && text.length > PASTE_BUFFER_THRESHOLD_BYTES) {
|
||||
e.preventDefault();
|
||||
loadIntoBuffer(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +290,7 @@ async function loadFileContents(file) {
|
||||
return;
|
||||
}
|
||||
|
||||
pasteArea.value = new TextDecoder().decode(content);
|
||||
reevaluateContentStatus();
|
||||
loadContent(new TextDecoder().decode(content));
|
||||
}
|
||||
|
||||
function selectLogFile() {
|
||||
@@ -355,8 +424,7 @@ async function handleDropEvent(e) {
|
||||
let files = e.dataTransfer.files;
|
||||
if (files.length !== 1) {
|
||||
if (Array.from(e.dataTransfer.types).includes('text/plain')) {
|
||||
pasteArea.value = e.dataTransfer.getData('text/plain');
|
||||
reevaluateContentStatus();
|
||||
loadContent(e.dataTransfer.getData('text/plain'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user