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:
Luca Rossi
2026-02-26 00:03:38 +01:00
committed by GitHub
parent 9e9096857d
commit ac4db43fd5
3 changed files with 54 additions and 5 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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])