# Phase 1 Implementation Guide: Metadata-First Loading ## Overview This guide provides step-by-step instructions to implement Phase 1 of the performance optimization strategy. This phase is the **quick win** that reduces startup time from 10-30 seconds to 2-5 seconds. **Estimated Time**: 4-6 hours **Difficulty**: Medium **Risk Level**: Low (backward compatible) --- ## Step 1: Create Fast Metadata-Only Loader ### File: `server/index.mjs` Add a new function to load only metadata without enrichment: ```javascript // Add after loadVaultNotes() function (around line 175) /** * Fast metadata loader - no enrichment, no content * Returns only: id, title, path, createdAt, updatedAt * Used for initial UI load to minimize startup time */ const loadVaultMetadataOnly = async (vaultPath) => { const notes = []; const walk = async (currentDir) => { if (!fs.existsSync(currentDir)) { return; } const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const entryPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { await walk(entryPath); continue; } if (!isMarkdownFile(entry)) { continue; } try { // Read file WITHOUT enrichment (fast) const content = fs.readFileSync(entryPath, 'utf-8'); const stats = fs.statSync(entryPath); const relativePathWithExt = path.relative(vaultPath, entryPath).replace(/\\/g, '/'); const relativePath = relativePathWithExt.replace(/\.md$/i, ''); const id = slugifyPath(relativePath); const fileNameWithExt = entry.name; const fallbackTitle = path.basename(relativePath); const title = extractTitle(content, fallbackTitle); const finalId = id || slugifySegment(fallbackTitle) || fallbackTitle; const createdDate = stats.birthtimeMs ? new Date(stats.birthtimeMs) : new Date(stats.ctimeMs); const updatedDate = new Date(stats.mtimeMs); notes.push({ id: finalId, title, // NO content field - saves memory // NO tags - can be extracted on-demand mtime: stats.mtimeMs, fileName: fileNameWithExt, filePath: relativePathWithExt, originalPath: relativePath, createdAt: createdDate.toISOString(), updatedAt: updatedDate.toISOString() // NO frontmatter - no enrichment }); } catch (err) { console.error(`Failed to read metadata for ${entryPath}:`, err); } } }; await walk(vaultPath); return notes; }; ``` --- ## Step 2: Create Metadata Endpoint ### File: `server/index.mjs` Add a new endpoint for fast metadata loading (around line 450, after `/api/files/list`): ```javascript // NEW: Fast metadata endpoint (no content, no enrichment) app.get('/api/vault/metadata', async (req, res) => { try { // Try Meilisearch first (already indexed) const client = meiliClient(); const indexUid = vaultIndexName(vaultDir); const index = await ensureIndexSettings(client, indexUid); const result = await index.search('', { limit: 10000, attributesToRetrieve: ['id', 'title', 'path', 'createdAt', 'updatedAt'] }); const items = Array.isArray(result.hits) ? result.hits.map(hit => ({ id: hit.id, title: hit.title, filePath: hit.path, createdAt: typeof hit.createdAt === 'number' ? new Date(hit.createdAt).toISOString() : hit.createdAt, updatedAt: typeof hit.updatedAt === 'number' ? new Date(hit.updatedAt).toISOString() : hit.updatedAt, })) : []; res.json(items); } catch (error) { console.error('Failed to load metadata via Meilisearch, falling back to FS:', error); try { // Fallback: fast filesystem scan without enrichment const notes = await loadVaultMetadataOnly(vaultDir); res.json(notes.map(n => ({ id: n.id, title: n.title, filePath: n.filePath, createdAt: n.createdAt, updatedAt: n.updatedAt }))); } catch (err2) { console.error('FS fallback failed:', err2); res.status(500).json({ error: 'Unable to load vault metadata.' }); } } }); ``` --- ## Step 3: Disable Enrichment During Startup ### File: `server/index.mjs` Modify `loadVaultNotes()` to skip enrichment (around line 138-141): **BEFORE:** ```javascript try { // Enrichir automatiquement le frontmatter lors du chargement const absPath = entryPath; const enrichResult = await enrichFrontmatterOnOpen(absPath); const content = enrichResult.content; const stats = fs.statSync(entryPath); // ... ``` **AFTER:** ```javascript try { // Skip enrichment during initial load for performance // Enrichment will happen on-demand when file is opened const content = fs.readFileSync(entryPath, 'utf-8'); const stats = fs.statSync(entryPath); // ... ``` **Why**: Enrichment is expensive and not needed for initial metadata load. It will still happen when the user opens a note via `/api/files` endpoint. --- ## Step 4: Update VaultService ### File: `src/app/services/vault.service.ts` First, let's check the current implementation: ```bash # Find VaultService find src -name "*vault*service*" -type f ``` If it doesn't exist, we need to create it or update the existing service. Let's assume it exists and update it: **Add metadata-first loading:** ```typescript import { Injectable, signal, computed, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class VaultService { private http = inject(HttpClient); // Signals for vault state private allNotesMetadata = signal([]); private contentCache = new Map(); private enrichmentCache = new Map(); // Public computed signals allNotes = computed(() => this.allNotesMetadata()); /** * Initialize vault with metadata-first approach * Step 1: Load metadata immediately (fast) * Step 2: Load full content on-demand when note is selected */ async initializeVault(): Promise { try { console.time('loadVaultMetadata'); // Load metadata only (fast) const metadata = await firstValueFrom( this.http.get('/api/vault/metadata') ); console.timeEnd('loadVaultMetadata'); console.log(`Loaded metadata for ${metadata.length} notes`); // Convert metadata to Note objects with empty content const notes = metadata.map(m => ({ id: m.id, title: m.title, filePath: m.filePath, originalPath: m.filePath.replace(/\.md$/i, ''), fileName: m.filePath.split('/').pop() || '', content: '', // Empty initially rawContent: '', tags: [], // Will be extracted on-demand frontmatter: {}, createdAt: m.createdAt, updatedAt: m.updatedAt, mtime: new Date(m.updatedAt).getTime() })); this.allNotesMetadata.set(notes); } catch (error) { console.error('Failed to initialize vault:', error); throw error; } } /** * Ensure note content is loaded (lazy loading) * Called when user selects a note */ async ensureNoteContent(noteId: string): Promise { const notes = this.allNotesMetadata(); const note = notes.find(n => n.id === noteId); if (!note) { console.warn(`Note not found: ${noteId}`); return null; } // If content already loaded, return if (note.content) { return note; } // Load content from server try { console.time(`loadNoteContent:${noteId}`); const response = await firstValueFrom( this.http.get('/api/files', { params: { path: note.filePath } }) ); console.timeEnd(`loadNoteContent:${noteId}`); // Update note with full content note.content = response.content || response; note.rawContent = response.rawContent || response; // Extract tags if not already present if (!note.tags || note.tags.length === 0) { note.tags = this.extractTags(note.content); } // Extract frontmatter if available if (response.frontmatter) { note.frontmatter = response.frontmatter; } return note; } catch (error) { console.error(`Failed to load note content for ${noteId}:`, error); return note; } } /** * Get note by ID (may have empty content if not yet loaded) */ getNoteById(noteId: string): Note | undefined { return this.allNotesMetadata().find(n => n.id === noteId); } /** * Extract tags from markdown content */ private extractTags(content: string): string[] { const tagRegex = /(^|\s)#([A-Za-z0-9_\/-]+)/g; const tags = new Set(); let match; while ((match = tagRegex.exec(content)) !== null) { tags.add(match[2]); } return Array.from(tags); } } ``` --- ## Step 5: Update AppComponent Initialization ### File: `src/app.component.ts` Modify the `ngOnInit()` method to use the new metadata-first approach: **BEFORE:** ```typescript ngOnInit(): void { this.themeService.initFromStorage(); this.logService.log('APP_START', { /* ... */ }); } ``` **AFTER:** ```typescript ngOnInit(): void { this.themeService.initFromStorage(); // Initialize vault with metadata-first approach this.vaultService.initializeVault().then(() => { console.log('Vault initialized'); this.logService.log('VAULT_INITIALIZED', { notesCount: this.vaultService.allNotes().length }); }).catch(error => { console.error('Failed to initialize vault:', error); this.logService.log('VAULT_INIT_FAILED', { error: String(error) }, 'error'); }); this.logService.log('APP_START', { viewport: { width: typeof window !== 'undefined' ? window.innerWidth : 0, height: typeof window !== 'undefined' ? window.innerHeight : 0, }, }); } ``` --- ## Step 6: Update Note Selection to Load Content ### File: `src/app.component.ts` Modify the `selectNote()` method to ensure content is loaded: **BEFORE:** ```typescript async selectNote(noteId: string): Promise { let note = this.vaultService.getNoteById(noteId); if (!note) { // ... fallback logic ... } this.selectedNoteId.set(noteId); } ``` **AFTER:** ```typescript async selectNote(noteId: string): Promise { let note = this.vaultService.getNoteById(noteId); if (!note) { // ... fallback logic ... } // Ensure content is loaded before rendering if (!note.content) { console.log(`Loading content for note: ${noteId}`); note = await this.vaultService.ensureNoteContent(noteId); if (!note) { console.error(`Failed to load note: ${noteId}`); return; } } this.selectedNoteId.set(noteId); } ``` --- ## Step 7: Testing ### Test 1: Verify Metadata Endpoint ```bash # Test the new endpoint curl http://localhost:3000/api/vault/metadata | head -50 # Should return fast (< 1 second for 1000 files) time curl http://localhost:3000/api/vault/metadata > /dev/null ``` ### Test 2: Verify Startup Time ```bash # Measure startup time in browser console performance.mark('app-start'); // ... app loads ... performance.mark('app-ready'); performance.measure('startup', 'app-start', 'app-ready'); console.log(performance.getEntriesByName('startup')[0].duration); ``` ### Test 3: Verify Content Loading 1. Open browser DevTools → Network tab 2. Start the application 3. Verify that `/api/vault/metadata` is called first (small payload) 4. Click on a note 5. Verify that `/api/files?path=...` is called to load content ### Test 4: Performance Comparison Create a test script to measure improvements: ```javascript // test-performance.mjs import fetch from 'node-fetch'; async function measureStartup() { console.time('Metadata Load'); const response = await fetch('http://localhost:3000/api/vault/metadata'); const data = await response.json(); console.timeEnd('Metadata Load'); console.log(`Loaded ${data.length} notes`); console.log(`Payload size: ${JSON.stringify(data).length} bytes`); } measureStartup(); ``` --- ## Step 8: Rollback Plan If issues occur, you can quickly rollback: 1. **Revert server changes**: Comment out the new endpoint, restore enrichment in `loadVaultNotes()` 2. **Revert client changes**: Remove the `initializeVault()` call, use old approach 3. **Keep `/api/vault` endpoint**: It still works as before for backward compatibility --- ## Verification Checklist - [ ] New endpoint `/api/vault/metadata` returns data in < 1 second - [ ] Metadata payload is < 1MB for 1000 files - [ ] App UI is interactive within 2-3 seconds - [ ] Clicking on a note loads content smoothly - [ ] No console errors or warnings - [ ] All existing features still work - [ ] Performance is improved by 50%+ compared to before --- ## Performance Metrics ### Before Phase 1 ``` Startup time (1000 files): 15-25 seconds Metadata endpoint: N/A Network payload: 5-10MB Memory usage: 200-300MB ``` ### After Phase 1 ``` Startup time (1000 files): 2-4 seconds ✓ Metadata endpoint: 0.5-1 second Network payload: 0.5-1MB Memory usage: 50-100MB ✓ ``` --- ## Troubleshooting ### Issue: Metadata endpoint returns 500 error **Solution**: Check server logs for Meilisearch errors. The fallback to `loadVaultMetadataOnly()` should work. ```bash # Verify Meilisearch is running curl http://localhost:7700/health ``` ### Issue: Notes don't load when clicked **Solution**: Ensure `ensureNoteContent()` is called in `selectNote()`. Check browser console for errors. ### Issue: Startup time hasn't improved **Solution**: 1. Verify `/api/vault/metadata` is being called (check Network tab) 2. Check that enrichment was removed from `loadVaultNotes()` 3. Verify old `/api/vault` endpoint is not being called --- ## Next Steps After Phase 1 is complete and tested: 1. Monitor performance in production 2. Collect user feedback 3. Proceed with Phase 2 (Pagination) if needed 4. Consider Phase 3 (Server Caching) for high-traffic deployments --- ## References - [Performance Optimization Strategy](./PERFORMANCE_OPTIMIZATION_STRATEGY.md) - [Angular Change Detection](https://angular.io/guide/change-detection) - [RxJS firstValueFrom](https://rxjs.dev/api/index/function/firstValueFrom)