perf: speed up Cmd+N note creation from ~150ms to ~16ms (#81)
* fix: speed up Cmd+N note creation from ~150ms to ~16ms Root cause: the editor focus used a fixed 150ms setTimeout after note creation, even when the BlockNote editor was already mounted. The optimistic UI (state updates, tab opening) was already synchronous, but the focus delay dominated perceived latency. Changes: - Editor focus now uses requestAnimationFrame when editor is mounted (~16ms on 60Hz), falling back to 80ms timeout for first-mount only - Rust save_note_content creates parent directories automatically, preventing optimistic-UI revert when creating notes in new folders - Added perf timing markers (console.debug) to measure creation→focus Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: retrigger after disk space cleanup --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -371,9 +371,16 @@ fn validate_save_path(file_path: &Path, display_path: &str) -> Result<(), String
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write content to a note file. Validates parent directory and read-only status.
|
||||
/// Write content to a note file. Creates parent directory if needed, validates path,
|
||||
/// then writes content to disk.
|
||||
pub fn save_note_content(path: &str, content: &str) -> Result<(), String> {
|
||||
let file_path = Path::new(path);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create directory {}: {}", parent.display(), e))?;
|
||||
}
|
||||
}
|
||||
validate_save_path(file_path, path)?;
|
||||
fs::write(file_path, content).map_err(|e| format!("Failed to save {}: {}", path, e))
|
||||
}
|
||||
@@ -960,6 +967,32 @@ References:
|
||||
assert!(entry.outgoing_links.contains(&"project/alpha".to_string()));
|
||||
}
|
||||
|
||||
// --- save_note_content tests ---
|
||||
|
||||
#[test]
|
||||
fn test_save_note_content_creates_parent_directory() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("new-type/untitled-note.md");
|
||||
let content = "---\ntitle: Untitled note\n---\n# Untitled note\n\n";
|
||||
|
||||
assert!(!path.parent().unwrap().exists());
|
||||
save_note_content(path.to_str().unwrap(), content).unwrap();
|
||||
|
||||
assert!(path.exists());
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_note_content_existing_directory() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
fs::create_dir_all(dir.path().join("note")).unwrap();
|
||||
let path = dir.path().join("note/test.md");
|
||||
let content = "# Test\n";
|
||||
|
||||
save_note_content(path.to_str().unwrap(), content).unwrap();
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), content);
|
||||
}
|
||||
|
||||
// Frontmatter update/delete tests are in frontmatter.rs
|
||||
// save_image tests are in vault/image.rs
|
||||
// purge_trash tests are in vault/trash.rs
|
||||
|
||||
@@ -429,10 +429,21 @@ export const Editor = memo(function Editor({
|
||||
tabPathsRef.current = currentPaths
|
||||
}, [tabs])
|
||||
|
||||
// Focus editor when a new note is created (signaled via custom event)
|
||||
// Focus editor when a new note is created (signaled via custom event).
|
||||
// Uses adaptive timing: fast rAF path when editor is already mounted,
|
||||
// short timeout when waiting for first mount.
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setTimeout(() => editor.focus(), 150)
|
||||
const handler = (e: Event) => {
|
||||
const t0 = (e as CustomEvent).detail?.t0 as number | undefined
|
||||
const doFocus = () => {
|
||||
editor.focus()
|
||||
if (t0) console.debug(`[perf] createNote → focus: ${(performance.now() - t0).toFixed(1)}ms`)
|
||||
}
|
||||
if (editorMountedRef.current) {
|
||||
requestAnimationFrame(doFocus)
|
||||
} else {
|
||||
setTimeout(doFocus, 80)
|
||||
}
|
||||
}
|
||||
window.addEventListener('laputa:focus-editor', handler)
|
||||
return () => window.removeEventListener('laputa:focus-editor', handler)
|
||||
|
||||
@@ -183,6 +183,11 @@ function navigateWikilink(entries: VaultEntry[], target: string, selectNote: (e:
|
||||
else console.warn(`Navigation target not found: ${target}`)
|
||||
}
|
||||
|
||||
/** Dispatch focus-editor event with perf timing marker. */
|
||||
function signalFocusEditor(): void {
|
||||
window.dispatchEvent(new CustomEvent('laputa:focus-editor', { detail: { t0: performance.now() } }))
|
||||
}
|
||||
|
||||
/** Persist to disk; on failure, call the revert handler. */
|
||||
function persistOptimistic(path: string, content: string, onFail: (p: string) => void): void {
|
||||
persistNewNote(path, content).catch(() => onFail(path))
|
||||
@@ -275,7 +280,7 @@ export function useNoteActions(config: NoteActionsConfig) {
|
||||
const title = generateUntitledName(entries, noteType, pendingNamesRef.current)
|
||||
pendingNamesRef.current.add(title)
|
||||
handleCreateNote(title, noteType)
|
||||
window.dispatchEvent(new CustomEvent('laputa:focus-editor'))
|
||||
signalFocusEditor()
|
||||
setTimeout(() => pendingNamesRef.current.delete(title), 500)
|
||||
}, [entries, handleCreateNote])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user