aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-16 10:51:05 -0800
committerGitHub <noreply@github.com>2026-02-16 10:51:05 -0800
commitc5fdd406503aaedaac5e117e710d7910112af83f (patch)
tree3a8b7b1d9277ba0db4c946fea43043d65220fd8d /frontend-vanilla
parent8b852475d7952f37b630480af76897baca06e937 (diff)
parentd4caf45b2b9ea6a3276de792cf6f73085e66b1ae (diff)
downloadneko-c5fdd406503aaedaac5e117e710d7910112af83f.tar.gz
neko-c5fdd406503aaedaac5e117e710d7910112af83f.tar.bz2
neko-c5fdd406503aaedaac5e117e710d7910112af83f.zip
Merge pull request #4 from adammathes/claude/performance-stress-testing-qQuV8
Add performance benchmarks, stress tests, and frontend perf tests
Diffstat (limited to 'frontend-vanilla')
-rw-r--r--frontend-vanilla/src/perf/renderItems.perf.test.ts90
-rw-r--r--frontend-vanilla/src/perf/store.perf.test.ts124
2 files changed, 214 insertions, 0 deletions
diff --git a/frontend-vanilla/src/perf/renderItems.perf.test.ts b/frontend-vanilla/src/perf/renderItems.perf.test.ts
new file mode 100644
index 0000000..3dd2ed7
--- /dev/null
+++ b/frontend-vanilla/src/perf/renderItems.perf.test.ts
@@ -0,0 +1,90 @@
+import { describe, it, expect } from 'vitest';
+import { createFeedItem } from '../components/FeedItem';
+import type { Item } from '../types';
+
+function makeItem(id: number): Item {
+ return {
+ _id: id,
+ feed_id: 1,
+ title: `Test Item ${id}`,
+ url: `https://example.com/item/${id}`,
+ description: `<p>Description for item ${id} with <b>bold</b> and <a href="https://example.com">link</a></p>`,
+ publish_date: '2024-01-01T00:00:00Z',
+ read: id % 3 === 0,
+ starred: id % 5 === 0,
+ feed_title: 'Test Feed',
+ };
+}
+
+describe('renderItems performance', () => {
+ it('createFeedItem renders 100 items under 50ms', () => {
+ const items = Array.from({ length: 100 }, (_, i) => makeItem(i));
+
+ const start = performance.now();
+ const html = items.map(item => createFeedItem(item)).join('');
+ const elapsed = performance.now() - start;
+
+ expect(html).toBeTruthy();
+ expect(html).toContain('feed-item');
+ expect(elapsed).toBeLessThan(50);
+ });
+
+ it('createFeedItem renders 500 items under 200ms', () => {
+ const items = Array.from({ length: 500 }, (_, i) => makeItem(i));
+
+ const start = performance.now();
+ const html = items.map(item => createFeedItem(item)).join('');
+ const elapsed = performance.now() - start;
+
+ expect(html).toBeTruthy();
+ expect(elapsed).toBeLessThan(200);
+ });
+
+ it('createFeedItem renders 1000 items under 100ms', () => {
+ const items = Array.from({ length: 1000 }, (_, i) => makeItem(i));
+
+ const start = performance.now();
+ const results: string[] = [];
+ for (const item of items) {
+ results.push(createFeedItem(item));
+ }
+ const elapsed = performance.now() - start;
+
+ expect(results.length).toBe(1000);
+ expect(elapsed).toBeLessThan(100);
+ });
+
+ it('DOM insertion of 100 items under 200ms', () => {
+ const items = Array.from({ length: 100 }, (_, i) => makeItem(i));
+ const html = items.map(item => createFeedItem(item)).join('');
+
+ const container = document.createElement('ul');
+ document.body.appendChild(container);
+
+ const start = performance.now();
+ container.innerHTML = html;
+ const elapsed = performance.now() - start;
+
+ expect(container.children.length).toBe(100);
+ expect(elapsed).toBeLessThan(200);
+
+ document.body.removeChild(container);
+ });
+
+ it('DOM insertion of 500 items under 500ms', () => {
+ const items = Array.from({ length: 500 }, (_, i) => makeItem(i));
+ const html = items.map(item => createFeedItem(item)).join('');
+
+ const container = document.createElement('ul');
+ document.body.appendChild(container);
+
+ const start = performance.now();
+ container.innerHTML = html;
+ const elapsed = performance.now() - start;
+
+ expect(container.children.length).toBe(500);
+ expect(elapsed).toBeLessThan(500);
+
+ document.body.removeChild(container);
+ });
+});
diff --git a/frontend-vanilla/src/perf/store.perf.test.ts b/frontend-vanilla/src/perf/store.perf.test.ts
new file mode 100644
index 0000000..734e132
--- /dev/null
+++ b/frontend-vanilla/src/perf/store.perf.test.ts
@@ -0,0 +1,124 @@
+import { describe, it, expect } from 'vitest';
+import { Store } from '../store';
+import type { Item, Feed, Category } from '../types';
+
+function makeItem(id: number): Item {
+ return {
+ _id: id,
+ feed_id: 1,
+ title: `Test Item ${id}`,
+ url: `https://example.com/item/${id}`,
+ description: `Description for item ${id}`,
+ publish_date: '2024-01-01T00:00:00Z',
+ read: false,
+ starred: false,
+ feed_title: 'Test Feed',
+ };
+}
+
+function makeFeed(id: number): Feed {
+ return {
+ _id: id,
+ url: `https://example.com/feed/${id}`,
+ web_url: `https://example.com/${id}`,
+ title: `Feed ${id}`,
+ category: `cat-${id % 5}`,
+ };
+}
+
+describe('store performance', () => {
+ it('setItems with 500 items + event dispatch under 10ms', () => {
+ const store = new Store();
+ const items = Array.from({ length: 500 }, (_, i) => makeItem(i));
+
+ let eventFired = false;
+ store.on('items-updated', () => { eventFired = true; });
+
+ const start = performance.now();
+ store.setItems(items);
+ const elapsed = performance.now() - start;
+
+ expect(store.items.length).toBe(500);
+ expect(eventFired).toBe(true);
+ expect(elapsed).toBeLessThan(10);
+ });
+
+ it('setItems append 500 items to existing 500 under 10ms', () => {
+ const store = new Store();
+ const initial = Array.from({ length: 500 }, (_, i) => makeItem(i));
+ const more = Array.from({ length: 500 }, (_, i) => makeItem(i + 500));
+
+ store.setItems(initial);
+
+ const start = performance.now();
+ store.setItems(more, true);
+ const elapsed = performance.now() - start;
+
+ expect(store.items.length).toBe(1000);
+ expect(elapsed).toBeLessThan(10);
+ });
+
+ it('setFeeds with 200 feeds under 5ms', () => {
+ const store = new Store();
+ const feeds = Array.from({ length: 200 }, (_, i) => makeFeed(i));
+
+ let eventFired = false;
+ store.on('feeds-updated', () => { eventFired = true; });
+
+ const start = performance.now();
+ store.setFeeds(feeds);
+ const elapsed = performance.now() - start;
+
+ expect(store.feeds.length).toBe(200);
+ expect(eventFired).toBe(true);
+ expect(elapsed).toBeLessThan(5);
+ });
+
+ it('rapid filter changes (100 toggles) under 50ms', () => {
+ const store = new Store();
+ const filters: Array<'unread' | 'all' | 'starred'> = ['unread', 'all', 'starred'];
+ let eventCount = 0;
+ store.on('filter-updated', () => { eventCount++; });
+
+ const start = performance.now();
+ for (let i = 0; i < 100; i++) {
+ store.setFilter(filters[i % 3]);
+ }
+ const elapsed = performance.now() - start;
+
+ expect(eventCount).toBeGreaterThan(0);
+ expect(elapsed).toBeLessThan(50);
+ });
+
+ it('rapid search query changes (100 updates) under 50ms', () => {
+ const store = new Store();
+ let eventCount = 0;
+ store.on('search-updated', () => { eventCount++; });
+
+ const start = performance.now();
+ for (let i = 0; i < 100; i++) {
+ store.setSearchQuery(`query-${i}`);
+ }
+ const elapsed = performance.now() - start;
+
+ expect(eventCount).toBe(100);
+ expect(elapsed).toBeLessThan(50);
+ });
+
+ it('multiple listeners (50) on items-updated under 10ms', () => {
+ const store = new Store();
+ const items = Array.from({ length: 100 }, (_, i) => makeItem(i));
+ let totalCalls = 0;
+
+ for (let i = 0; i < 50; i++) {
+ store.on('items-updated', () => { totalCalls++; });
+ }
+
+ const start = performance.now();
+ store.setItems(items);
+ const elapsed = performance.now() - start;
+
+ expect(totalCalls).toBe(50);
+ expect(elapsed).toBeLessThan(10);
+ });
+});