diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-16 08:57:29 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-16 08:57:29 -0800 |
| commit | 466804699bf42e403913d2fb435327b554d537d1 (patch) | |
| tree | f6732ccdc8d83f2f9ca5f8e045c07625616044d9 | |
| parent | aa42040ad31c689e82b0051dc9996c51286d1842 (diff) | |
| download | neko-466804699bf42e403913d2fb435327b554d537d1.tar.gz neko-466804699bf42e403913d2fb435327b554d537d1.tar.bz2 neko-466804699bf42e403913d2fb435327b554d537d1.zip | |
move docs to DOCS dir
| -rw-r--r-- | DOCS/performance_analysis.md | 99 | ||||
| -rw-r--r-- | DOCS/vanilla_frontend_proposal.md | 144 |
2 files changed, 243 insertions, 0 deletions
diff --git a/DOCS/performance_analysis.md b/DOCS/performance_analysis.md new file mode 100644 index 0000000..41f0bb9 --- /dev/null +++ b/DOCS/performance_analysis.md @@ -0,0 +1,99 @@ +# Performance Analysis & Recommendations + +## Executive Summary +You are correct that the React implementation introduces inherent latency compared to the vanilla JS/Backbone version, particularly during initial load and interactions. While backend measurements show fast response times (~5ms) for the current dataset (where `full_content` is mostly empty), the React frontend's rendering strategy and bundle overhead are the primary contributors to the perceived slowness. + +## Key Findings + +### 1. React Rendering Overhead vs. Vanilla JS +- **Initial Load**: The React application requires downloading a larger bundle (~241KB vs ~150KB for legacy) and then executing hydration before fetching data. This adds an initial delay (Time to Interactive) not present in the lightweight vanilla version. +- **Excessive Re-rendering**: The current `FeedItems` component triggers a re-render of the **entire list** (and all children `FeedItem` components) whenever a single item is selected or modified. React's reconciliation process diffs every item, which is computationally expensive compared to Backbone's direct DOM manipulation of a single element. +- **DOM Node Count**: Even with small content, rendering 15+ complex article cards with `dangerouslySetInnerHTML` involves significant DOM operations. React's synthetic event system and component overhead add micro-latency per item. + +### 2. Implementation Inefficiencies +- **Observer Churn**: The `IntersectionObserver` in `FeedItems.tsx` disconnects and reconnects for *every item* whenever the list updates or state changes. This is an O(N) operation that degrades performance as the list grows. +- **Lack of Memoization**: `FeedItem` components are not memoized (`React.memo`), causing them to re-render unnecessarily on parent state changes (like selecting an item with `j`/`k`). + +### 3. Backend Data Strategy (Minor Current Impact, Major Risk) +- Although `full_content` is currently empty for most items, the backend still retrieves and serializes this field for every item in the list. This is "fast enough" now (5ms) but remains a structural inefficiency that will cause severe slowdowns if/when content is scraped. + +## Recommendations + +### 1. Optimize React Rendering (High Impact) +The most effective way to restore "snappiness" is to reduce React's work: +- **Memoize `FeedItem`**: Wrap the component in `React.memo` so it only re-renders when its specific props change. This prevents the entire list from flashing when you select one item. +- **Virtualize Long Lists**: Implement `react-window` or `virtuoso` for both the sidebar (`FeedList`) and main view (`FeedItems`). This ensures only visible items are in the DOM, keeping the browser responsive regardless of list size. +- **Stable References**: Use `useCallback` for event handlers passed to children to prevent breaking memoization. + +### 2. Fix Observer & Effect Logic (Medium Impact) +- Refactor the `IntersectionObserver` in `FeedItems.tsx` to maintain a stable observer instance using `useRef` and only observe/unobserve specific elements as needed, rather than resetting the whole list. + +### 3. Backend Optimization (Defensive) +- Even if not the current bottleneck, modifying `item.Filter` to exclude `full_content` on list views is a simple change that prevents future performance regressions. + +## Performance Optimizations Implemented (Current Session) + +Following the analysis above, the following optimizations have been applied to the React frontend: + +### 1. Component Memoization +- **Change**: `FeedItem` is now wrapped in `React.memo`, and event handlers (`onToggleStar`, `onUpdate`) are memoized with `useCallback`. +- **Impact**: Previously, clicking an item or pressing 'j'/'k' caused the **entire list** of items to re-render because the parent `FeedItems` state changed. Now, only the specific item being modified re-renders. This transforms interaction complexity from **O(N)** to **O(1)**, significantly improving apparent responsiveness. + +### 2. Stable IntersectionObserver +- **Change**: The `IntersectionObserver` in `FeedItems` now uses a `useRef` to maintain a stable instance. It no longer disconnects and reconnects on every render or state change. The observer now references current state via refs (`itemsRef`, `hasMoreRef`) to avoid stale closures without triggering effect re-execution. +- **Impact**: Removes the overhead of constantly destroying and recreating overlapping observers. Scrolling performance is smoother, and the "read" marking logic is more reliable and efficient. + +### 3. Event Handler Optimization +- **Change**: Keyboard event handlers now use `refs` to access the latest state (`items`, `hasMore`, `loadingMore`) without needing to be re-attached on every render. +- **Impact**: Reduces React's internal bookkeeping overhead and prevents event listener churn. + +--- + +## Proposal: "Vanilla JS Optimized" Frontend (Modern Backdrop) + +Given your preference for the responsiveness of the legacy Backbone version, a modern Vanilla JS approach could offer the best of both worlds: the raw speed of direct DOM manipulation with the maintainability of modern ES6+ standards. + +### Core Philosophy +**"No Framework, Just Platform."** +Instead of React's Virtual DOM diffing (which adds overhead), we check state and update *only* the specific DOM nodes that change. + +### Architecture Proposal + +1. **State Management**: + - Use a simple **Store Pattern** using ES6 `Proxy` or a lightweight `Pub/Sub` module. + - State changes (e.g., `store.items[0].read = true`) automatically trigger specific DOM updates via subscribers, without re-evaluating a component tree. + +2. **Rendering**: + - **Initial Render**: Use **Tagged Template Literals** (like `lit-html` but simpler) to generate HTML strings efficiently. + ```javascript + const itemTemplate = (item) => ` + <li class="feed-item ${item.read ? 'read' : ''}" data-id="${item.id}"> + ... + </li>`; + ul.innerHTML = items.map(itemTemplate).join(''); + ``` + - **Updates**: Direct DOM manipulation. + ```javascript + // When item 101 becomes read: + document.querySelector(`li[data-id="101"]`).classList.add('read'); + ``` + - **Benefits**: Zero diffing cost. Instant updates. + +3. **Component Structure**: + - Use **Web Components (Custom Elements)** for encapsulated, reusable UI elements (e.g., `<feed-item>`). + - Native browser support means no framework overhead. + +4. **Routing**: + - Lightweight wrapper around **History API** to handle URL changes and view swapping without full reloads. + +5. **Build Tooling**: + - **Vite** (for dev server/HMR) but configured to bundle vanilla JS. + - **CSS**: Standard CSS variables for theming (already partially in place). + +### Why This Beats React for Neko +- **Zero Hydration**: The browser parses HTML and executes minimal JS. TTI is effectively immediate. +- **Memory Footprint**: No Virtual DOM copies of the tree. +- **Predictable Performance**: You control exactly when and how the DOM updates, mirroring the Backbone experience but with cleaner, modern code. + +## Conclusion +The "sluggishness" was primarily due to React's re-rendering of the entire list on interactions and the initial hydration cost. The **Memoization** and **IntersectionObserver** optimizations implemented have significantly reduced the re-rendering overhead, bringing responsiveness closer to the vanilla JS experience. However, a move to a modern Vanilla JS architecture (as proposed) would eliminate the inherent framework overhead entirely. diff --git a/DOCS/vanilla_frontend_proposal.md b/DOCS/vanilla_frontend_proposal.md new file mode 100644 index 0000000..de7e1dc --- /dev/null +++ b/DOCS/vanilla_frontend_proposal.md @@ -0,0 +1,144 @@ +# Vanilla JS Frontend Implementation Proposal + +## 1. Executive Summary +This proposal outlines the strategy to build a lightweight, high-performance "v3" frontend for Neko using modern Vanilla JavaScript (ES Modules, Web Components). The goal is to achieve the instant responsiveness of the legacy Backbone version while maintaining code maintainability and modern best practices. This version will coexist with the current React ("v2") frontend, allowing for A/B testing and gradual migration. + +## 2. Architecture + +### 2.1 Core Philosophy +- **No Frameworks**: Remove the overhead of React, Virtual DOM, and hydration. +- **Platform First**: Use standard Web APIs (Custom Elements, IntersectionObserver, Fetch). +- **Direct DOM Manipulation**: Update only what changes. State changes map 1:1 to DOM class/attribute updates. + +### 2.2 Directory Structure +A new directory `frontend-vanilla` will be created alongside the existing `frontend`: +``` +neko/ +├── frontend/ # Current React V2 +├── frontend-vanilla/ # New Vanilla V3 +│ ├── index.html +│ ├── src/ +│ │ ├── components/ +│ │ ├── store.ts # State management +│ │ ├── router.ts # History API wrapper +│ │ └── main.ts +│ └── vite.config.ts +└── web/ # Go backend serving logic +``` + +### 2.3 Technology Stack +- **Build Tool**: Vite (Vanilla preset). Extremely fast dev server and optimized bundling. +- **Language**: TypeScript (sharing types with `frontend` where possible `../frontend/src/types`). +- **CSS**: Standard CSS Variables (sharing `index.css` concepts from v2). +- **State**: Custom lightweight `Pub/Sub` Store. + +## 3. Coexistence Strategy + +To support both versions simultaneously during development and review: + +### 3.1 Backend Serving (`web/web.go`) +1. **Build Output**: `frontend-vanilla` will build to `web/dist/v3`. +2. **Embedding**: Update `web.go` to embed the new directory. + ```go + //go:embed dist/v2/* dist/v3/* + frontendFiles embed.FS + ``` +3. **Routing**: + - Host v3 at `/v3/`. + - Users can switch via a simple link in the Settings menu (or a UI toggle). + - Feature flag (cookie) can serve v3 at `/` if desired later. + +### 3.2 Development Helper +- Add a `make dev-vanilla` target that runs the Vite dev server for the vanilla app, proxying API requests to the Go backend. + +## 4. Implementation Details + +### 4.1 State Management (The Store) +A simple `Store` class that extends `EventTarget` or uses a basic subscription model. +```typescript +class Store { + items: Item[] = []; + events = new EventTarget(); + + setItems(items: Item[]) { + this.items = items; + this.events.dispatchEvent(new CustomEvent('update')); + } + + markRead(id: number) { + const item = this.items.find(i => i._id === id); + if (item) { + item.read = true; + // Emit specific event for precise DOM update + this.events.dispatchEvent(new CustomEvent('item-change', { detail: { id, change: 'read' } })); + // Sync with API... + } + } +} +``` + +### 4.2 Efficient Rendering +- **Initial Render**: Use Template Literals mapped to arrays. This is the fastest way to generate HTML strings in JS. +- **Updates**: Components subscribe to the Store. When `item-change` fires for ID 101, the `<feed-item id="101">` component updates its *own* class list directly. No parent re-render. + +### 4.3 Web Components +Encapsulate logic in Custom Elements to keep code organized. +```typescript +class FeedItem extends HTMLElement { + connectedCallback() { + // Render content + // Subscribe to store updates for self + } + // ... +} +customElements.define('feed-item', FeedItem); +``` + +## 5. Testing Strategy + +### 5.1 Unit Testing +- **Tool**: Vitest. +- **Approach**: Test "business logic" (Store, Router) in isolation. Test components using `JSDOM`. +- **Benefit**: Fastest feedback loop. + +### 5.2 E2E Testing (The "Compliance" Suite) +- **Tool**: Playwright (Reuse existing suite). +- **Strategy**: + - Update existing tests to use agnostic data-attributes (e.g., `data-test-id="feed-item"`). + - Run the *same* test suite against both `/v2/` (React) and `/v3/` (Vanilla) endpoints. + - This guarantees feature parity. + +```typescript +// example-spec.ts +const uiVersion = process.env.UI_VERSION || 'v2'; // or v3 + +test('loads items', async ({ page }) => { + await page.goto(`/${uiVersion}/`); + // assertions... +}); +``` + +## 6. Work Plan + +1. **Phase 1: Scaffold & Embed (1 Day)** + - Initialize `frontend-vanilla`. + - Update `web.go` to serve it. + - Verify "Hello World" at `/v3/`. + +2. **Phase 2: Read-Only Implementation (2 Days)** + - Implement fetching feeds/items. + - Implement Store and Template rendering. + - **Milestone**: Benchmark loading execution time vs React. + +3. **Phase 3: Interactive Features (2 Days)** + - Implement Keyboard navigation (j/k). + - Implement Mark as Read / Star. + - Implement Infinite Scroll. + +4. **Phase 4: Parity & Polish (Ongoing)** + - Settings, Mobile Sidebar, etc. + - Passing Compliance Tests. + +## 7. Next Steps for Approval +- Review and approve this proposal. +- Authorize creation of `frontend-vanilla` directory. |
