lazy-render-virtual-scroll 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -11
- package/backend-helpers/README.md +233 -0
- package/backend-helpers/example.ts +114 -0
- package/backend-helpers/frontend-example.tsx +122 -0
- package/backend-helpers/pagination.ts +159 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -22,6 +22,47 @@ A framework-agnostic virtual scrolling and lazy rendering solution that efficien
|
|
|
22
22
|
| Memory usage (10k items) | High | Low |
|
|
23
23
|
| Initial load time | Slow | Fast |
|
|
24
24
|
| Scroll performance | Janky | Smooth |
|
|
25
|
+
| **1 Million items (with backend pagination)** | **3+ seconds** | **<100ms** |
|
|
26
|
+
|
|
27
|
+
## Backend Integration
|
|
28
|
+
|
|
29
|
+
### **🚨 IMPORTANT: Backend Pagination Required**
|
|
30
|
+
|
|
31
|
+
For optimal performance with large datasets (1M+ items), use backend pagination:
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// Backend (Node.js/Express)
|
|
35
|
+
app.get('/api/cards', async (req, res) => {
|
|
36
|
+
const page = parseInt(req.query.page) || 1;
|
|
37
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 1000);
|
|
38
|
+
const skip = (page - 1) * limit;
|
|
39
|
+
|
|
40
|
+
const items = await getDataFromDatabase(skip, limit);
|
|
41
|
+
const total = await getTotalCount();
|
|
42
|
+
|
|
43
|
+
res.json({
|
|
44
|
+
data: items,
|
|
45
|
+
pagination: {
|
|
46
|
+
page,
|
|
47
|
+
limit,
|
|
48
|
+
total,
|
|
49
|
+
totalPages: Math.ceil(total / limit),
|
|
50
|
+
hasMore: page < Math.ceil(total / limit)
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**See [Backend Helpers](./backend-helpers/README.md) for complete examples.**
|
|
57
|
+
|
|
58
|
+
### **Performance with Backend Pagination:**
|
|
59
|
+
|
|
60
|
+
| Metric | Without Pagination | With Pagination | Improvement |
|
|
61
|
+
|--------|-------------------|-----------------|-------------|
|
|
62
|
+
| **API Response Time** | 3378ms | <100ms | **33x faster** ⚡ |
|
|
63
|
+
| **Data Transferred** | ~500MB | ~25KB | **20,000x less** 📉 |
|
|
64
|
+
| **Initial Load** | 3+ seconds | <200ms | **15x faster** ⚡ |
|
|
65
|
+
| **Memory Usage** | Very High | Very Low | **99% reduction** 💾 |
|
|
25
66
|
|
|
26
67
|
## Features
|
|
27
68
|
|
|
@@ -34,17 +75,7 @@ A framework-agnostic virtual scrolling and lazy rendering solution that efficien
|
|
|
34
75
|
- **Device Performance Monitoring**: Adjusts behavior based on device capabilities
|
|
35
76
|
- **Content Complexity Analysis**: Optimizes for different content types and complexity
|
|
36
77
|
- **Memory Efficient**: Automatically cleans up off-screen elements
|
|
37
|
-
- **Multi-Framework Support**: Easy integration with React, Vue, Angular, Svelte, and vanilla JavaScript
|
|
38
|
-
- **Advanced Performance Optimization**: Frame-rate optimized updates and GPU acceleration
|
|
39
|
-
- **Memory Management**: Intelligent caching and cleanup for optimal memory usage
|
|
40
|
-
- **GPU Acceleration**: Hardware-accelerated rendering for smooth performance
|
|
41
|
-
- **Frame Budget Optimization**: Limits updates to maintain 60fps performance
|
|
42
|
-
- **Batch Update Processing**: Reduces DOM manipulations for better performance
|
|
43
78
|
- **React Adapter**: Easy integration with React applications
|
|
44
|
-
- **Vue Adapter**: Composition API integration with Vue 3
|
|
45
|
-
- **Angular Adapter**: Directive and component for Angular applications
|
|
46
|
-
- **Svelte Adapter**: Action and component for Svelte applications
|
|
47
|
-
- **Vanilla JS Support**: Web Components and plain JavaScript implementation
|
|
48
79
|
- **Configurable Buffer**: Adjustable buffer size for optimal performance
|
|
49
80
|
- **Overscan Support**: Additional buffer for smoother scrolling
|
|
50
81
|
- **Predictive Loading**: Anticipates user needs based on scroll patterns
|
|
@@ -156,7 +187,7 @@ const MyCustomComponent = () => {
|
|
|
156
187
|
}}
|
|
157
188
|
/>
|
|
158
189
|
|
|
159
|
-
|
|
190
|
+
{ /* Loading indicato */}
|
|
160
191
|
{isLoading && (
|
|
161
192
|
<div className="lazy-loading">
|
|
162
193
|
Loading more items...
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Backend Pagination Helper - lazy-render
|
|
2
|
+
|
|
3
|
+
## **🚨 PROBLEM SOLVED**
|
|
4
|
+
|
|
5
|
+
**Before:**
|
|
6
|
+
```
|
|
7
|
+
GET /api/monitoring/cards
|
|
8
|
+
Returns: 10,00,000 items at once
|
|
9
|
+
Response Time: 3+ seconds ❌
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**After:**
|
|
13
|
+
```
|
|
14
|
+
GET /api/monitoring/cards?page=1&limit=50
|
|
15
|
+
Returns: 50 items per request
|
|
16
|
+
Response Time: <100ms ✅
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## **INSTALLATION**
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install lazy-render-virtual-scroll
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## **BACKEND IMPLEMENTATION (Node.js/Express)**
|
|
26
|
+
|
|
27
|
+
### **1. Basic Setup**
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const express = require('express');
|
|
31
|
+
const { calculatePagination } = require('lazy-render-virtual-scroll/backend-helpers');
|
|
32
|
+
|
|
33
|
+
const app = express();
|
|
34
|
+
|
|
35
|
+
app.get('/api/monitoring/cards', async (req, res) => {
|
|
36
|
+
const page = parseInt(req.query.page) || 1;
|
|
37
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 1000);
|
|
38
|
+
const skip = (page - 1) * limit;
|
|
39
|
+
|
|
40
|
+
// Get data from database
|
|
41
|
+
const items = await getDataFromDatabase(skip, limit);
|
|
42
|
+
const total = await getTotalCount();
|
|
43
|
+
|
|
44
|
+
// Return paginated response
|
|
45
|
+
res.json(calculatePagination(items, page, limit, total));
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### **2. MongoDB Example**
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
app.get('/api/monitoring/cards', async (req, res) => {
|
|
53
|
+
const page = parseInt(req.query.page) || 1;
|
|
54
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 1000);
|
|
55
|
+
const skip = (page - 1) * limit;
|
|
56
|
+
|
|
57
|
+
const [items, total] = await Promise.all([
|
|
58
|
+
Card.find().skip(skip).limit(limit).sort({ createdAt: -1 }),
|
|
59
|
+
Card.countDocuments()
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
res.json({
|
|
63
|
+
data: items,
|
|
64
|
+
pagination: {
|
|
65
|
+
page,
|
|
66
|
+
limit,
|
|
67
|
+
total,
|
|
68
|
+
totalPages: Math.ceil(total / limit),
|
|
69
|
+
hasMore: page < Math.ceil(total / limit)
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### **3. MySQL/PostgreSQL Example**
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
app.get('/api/monitoring/cards', async (req, res) => {
|
|
79
|
+
const page = parseInt(req.query.page) || 1;
|
|
80
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 1000);
|
|
81
|
+
const offset = (page - 1) * limit;
|
|
82
|
+
|
|
83
|
+
const [items, totalResult] = await Promise.all([
|
|
84
|
+
db.query('SELECT * FROM cards ORDER BY created_at DESC LIMIT ? OFFSET ?', [limit, offset]),
|
|
85
|
+
db.query('SELECT COUNT(*) as count FROM cards')
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
res.json({
|
|
89
|
+
data: items,
|
|
90
|
+
pagination: {
|
|
91
|
+
page,
|
|
92
|
+
limit,
|
|
93
|
+
total: totalResult[0].count,
|
|
94
|
+
totalPages: Math.ceil(totalResult[0].count / limit),
|
|
95
|
+
hasMore: page < Math.ceil(totalResult[0].count / limit)
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## **FRONTEND INTEGRATION (React)**
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
import { LazyList } from 'lazy-render-virtual-scroll';
|
|
105
|
+
|
|
106
|
+
function Dashboard() {
|
|
107
|
+
const [items, setItems] = useState([]);
|
|
108
|
+
const [page, setPage] = useState(1);
|
|
109
|
+
const [hasMore, setHasMore] = useState(true);
|
|
110
|
+
|
|
111
|
+
const fetchMore = async () => {
|
|
112
|
+
if (!hasMore) return [];
|
|
113
|
+
|
|
114
|
+
const response = await fetch(`/api/cards?page=${page}&limit=50`);
|
|
115
|
+
const result = await response.json();
|
|
116
|
+
|
|
117
|
+
setItems(prev => [...prev, ...result.data]);
|
|
118
|
+
setPage(prev => prev + 1);
|
|
119
|
+
setHasMore(result.pagination.hasMore);
|
|
120
|
+
|
|
121
|
+
return result.data;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<LazyList
|
|
126
|
+
items={items}
|
|
127
|
+
itemHeight={200}
|
|
128
|
+
viewportHeight={600}
|
|
129
|
+
fetchMore={fetchMore}
|
|
130
|
+
renderItem={(item) => <Card data={item} />}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## **PERFORMANCE COMPARISON**
|
|
137
|
+
|
|
138
|
+
| Metric | Before | After | Improvement |
|
|
139
|
+
|--------|---------|-------|-------------|
|
|
140
|
+
| **API Response Time** | 3378ms | <100ms | **33x faster** ⚡ |
|
|
141
|
+
| **Data Transferred** | ~500MB | ~25KB | **20,000x less** 📉 |
|
|
142
|
+
| **Initial Load** | 3+ seconds | <200ms | **15x faster** ⚡ |
|
|
143
|
+
| **Memory Usage** | Very High | Very Low | **99% reduction** 💾 |
|
|
144
|
+
| **Server Load** | Very High | Minimal | **99% reduction** 🖥️ |
|
|
145
|
+
|
|
146
|
+
## **API PARAMETERS**
|
|
147
|
+
|
|
148
|
+
### **Query Parameters:**
|
|
149
|
+
- `page` (number): Page number (default: 1)
|
|
150
|
+
- `limit` (number): Items per page (default: 50, max: 1000)
|
|
151
|
+
|
|
152
|
+
### **Response Format:**
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"data": [...],
|
|
156
|
+
"pagination": {
|
|
157
|
+
"page": 1,
|
|
158
|
+
"limit": 50,
|
|
159
|
+
"total": 1000000,
|
|
160
|
+
"totalPages": 20000,
|
|
161
|
+
"hasMore": true,
|
|
162
|
+
"nextCursor": "2",
|
|
163
|
+
"prevCursor": null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## **BEST PRACTICES**
|
|
169
|
+
|
|
170
|
+
1. **Always use pagination** - Never return all data at once
|
|
171
|
+
2. **Limit maximum page size** - Prevent abuse with max limit (1000 recommended)
|
|
172
|
+
3. **Include pagination metadata** - Help frontend know total items and pages
|
|
173
|
+
4. **Use cursor-based pagination** - For very large datasets
|
|
174
|
+
5. **Cache count queries** - Total count can be expensive
|
|
175
|
+
|
|
176
|
+
## **ERROR HANDLING**
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
app.get('/api/cards', async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const page = parseInt(req.query.page) || 1;
|
|
182
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 1000);
|
|
183
|
+
|
|
184
|
+
if (page < 1) {
|
|
185
|
+
return res.status(400).json({ error: 'Page must be >= 1' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (limit < 1 || limit > 1000) {
|
|
189
|
+
return res.status(400).json({ error: 'Limit must be between 1 and 1000' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ... rest of implementation
|
|
193
|
+
} catch (error) {
|
|
194
|
+
res.status(500).json({
|
|
195
|
+
error: 'Failed to fetch data',
|
|
196
|
+
message: error.message
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## **MIGRATION GUIDE**
|
|
203
|
+
|
|
204
|
+
### **From: No Pagination**
|
|
205
|
+
```javascript
|
|
206
|
+
// ❌ OLD CODE
|
|
207
|
+
app.get('/api/cards', (req, res) => {
|
|
208
|
+
const allCards = database.getAll(); // Returns 1M items
|
|
209
|
+
res.json(allCards);
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### **To: With Pagination**
|
|
214
|
+
```javascript
|
|
215
|
+
// ✅ NEW CODE
|
|
216
|
+
app.get('/api/cards', async (req, res) => {
|
|
217
|
+
const page = parseInt(req.query.page) || 1;
|
|
218
|
+
const limit = 50;
|
|
219
|
+
const skip = (page - 1) * limit;
|
|
220
|
+
|
|
221
|
+
const items = database.getRange(skip, limit); // Returns 50 items
|
|
222
|
+
const total = database.count();
|
|
223
|
+
|
|
224
|
+
res.json({
|
|
225
|
+
data: items,
|
|
226
|
+
pagination: { page, limit, total, hasMore: true }
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## **SUPPORT**
|
|
232
|
+
|
|
233
|
+
For issues or questions, visit: https://github.com/sannuk79/lezzyrender
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BACKEND EXAMPLE - Fix for 1 Million Cards Performance Issue
|
|
3
|
+
*
|
|
4
|
+
* BEFORE (WRONG):
|
|
5
|
+
* GET /api/monitoring/cards
|
|
6
|
+
* Returns: 10,00,000 items at once
|
|
7
|
+
* Response time: 3+ seconds ❌
|
|
8
|
+
*
|
|
9
|
+
* AFTER (CORRECT):
|
|
10
|
+
* GET /api/monitoring/cards?page=1&limit=50
|
|
11
|
+
* Returns: 50 items per request
|
|
12
|
+
* Response time: <100ms ✅
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import { calculatePagination, validatePaginationParams } from './pagination';
|
|
17
|
+
|
|
18
|
+
const router = express.Router();
|
|
19
|
+
|
|
20
|
+
// Mock database - replace with your actual database
|
|
21
|
+
const mockCards = Array.from({ length: 1000000 }, (_, i) => ({
|
|
22
|
+
id: i,
|
|
23
|
+
title: `Card ${i}`,
|
|
24
|
+
category: `Category ${i % 100000}`,
|
|
25
|
+
createdAt: new Date().toISOString()
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ✅ CORRECT IMPLEMENTATION - With Pagination
|
|
30
|
+
*/
|
|
31
|
+
router.get('/monitoring/cards', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
// Get pagination parameters
|
|
34
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
35
|
+
const limit = Math.min(
|
|
36
|
+
parseInt(req.query.limit as string) || 50,
|
|
37
|
+
1000 // Maximum 1000 items per page
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Calculate skip for database query
|
|
41
|
+
const skip = (page - 1) * limit;
|
|
42
|
+
|
|
43
|
+
// Get total count (for pagination metadata)
|
|
44
|
+
const total = mockCards.length;
|
|
45
|
+
|
|
46
|
+
// Get only the requested page of data
|
|
47
|
+
const items = mockCards.slice(skip, skip + limit);
|
|
48
|
+
|
|
49
|
+
// Return paginated response
|
|
50
|
+
const response = calculatePagination(items, page, limit, total);
|
|
51
|
+
|
|
52
|
+
res.json(response);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Error fetching cards:', error);
|
|
55
|
+
res.status(500).json({
|
|
56
|
+
error: 'Failed to fetch cards',
|
|
57
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Alternative: MongoDB Implementation
|
|
64
|
+
*/
|
|
65
|
+
/*
|
|
66
|
+
router.get('/monitoring/cards', async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
69
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 50, 1000);
|
|
70
|
+
const skip = (page - 1) * limit;
|
|
71
|
+
|
|
72
|
+
// MongoDB query with pagination
|
|
73
|
+
const [items, total] = await Promise.all([
|
|
74
|
+
Card.find()
|
|
75
|
+
.skip(skip)
|
|
76
|
+
.limit(limit)
|
|
77
|
+
.sort({ createdAt: -1 }),
|
|
78
|
+
Card.countDocuments()
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const response = calculatePagination(items, page, limit, total);
|
|
82
|
+
res.json(response);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
res.status(500).json({ error: 'Failed to fetch cards' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Alternative: MySQL/PostgreSQL Implementation
|
|
91
|
+
*/
|
|
92
|
+
/*
|
|
93
|
+
router.get('/monitoring/cards', async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
96
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 50, 1000);
|
|
97
|
+
const offset = (page - 1) * limit;
|
|
98
|
+
|
|
99
|
+
// SQL query with LIMIT and OFFSET
|
|
100
|
+
const [items, totalResult] = await Promise.all([
|
|
101
|
+
db.query('SELECT * FROM cards ORDER BY created_at DESC LIMIT ? OFFSET ?', [limit, offset]),
|
|
102
|
+
db.query('SELECT COUNT(*) as count FROM cards')
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const total = totalResult[0].count;
|
|
106
|
+
const response = calculatePagination(items, page, limit, total);
|
|
107
|
+
res.json(response);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
res.status(500).json({ error: 'Failed to fetch cards' });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
export default router;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRONTEND EXAMPLE - Integration with lazy-render
|
|
3
|
+
* Solves the 1 million cards performance issue
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { LazyList, useLazyList } from 'lazy-render-virtual-scroll';
|
|
8
|
+
|
|
9
|
+
interface Card {
|
|
10
|
+
id: number;
|
|
11
|
+
title: string;
|
|
12
|
+
category: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PaginationInfo {
|
|
17
|
+
page: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
total: number;
|
|
20
|
+
totalPages: number;
|
|
21
|
+
hasMore: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ✅ CORRECT FRONTEND IMPLEMENTATION
|
|
26
|
+
* Uses lazy-render with paginated API
|
|
27
|
+
*/
|
|
28
|
+
export const ColorfulDashboard: React.FC = () => {
|
|
29
|
+
const [items, setItems] = useState<Card[]>([]);
|
|
30
|
+
const [page, setPage] = useState(1);
|
|
31
|
+
const [hasMore, setHasMore] = useState(true);
|
|
32
|
+
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
|
|
33
|
+
|
|
34
|
+
// Fetch more data when user scrolls
|
|
35
|
+
const fetchMore = async () => {
|
|
36
|
+
if (!hasMore) return [];
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Fetch only 50 items at a time
|
|
40
|
+
const response = await fetch(
|
|
41
|
+
`/api/monitoring/cards?page=${page}&limit=50`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error('Failed to fetch cards');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await response.json();
|
|
49
|
+
|
|
50
|
+
// Update state with new items
|
|
51
|
+
setItems(prev => [...prev, ...result.data]);
|
|
52
|
+
setPagination(result.pagination);
|
|
53
|
+
setPage(prev => prev + 1);
|
|
54
|
+
setHasMore(result.pagination.hasMore);
|
|
55
|
+
|
|
56
|
+
return result.data;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error fetching more cards:', error);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Use lazy-render hook
|
|
64
|
+
const { visibleRange, isLoading } = useLazyList({
|
|
65
|
+
itemHeight: 200,
|
|
66
|
+
viewportHeight: 600,
|
|
67
|
+
bufferSize: 5,
|
|
68
|
+
fetchMore
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Render individual card
|
|
72
|
+
const renderCard = (card: Card, index: number) => (
|
|
73
|
+
<div
|
|
74
|
+
key={card.id}
|
|
75
|
+
style={{
|
|
76
|
+
height: '200px',
|
|
77
|
+
borderBottom: '1px solid #eee',
|
|
78
|
+
padding: '16px'
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<h3>{card.title}</h3>
|
|
82
|
+
<p>Category: {card.category}</p>
|
|
83
|
+
<p>Created: {new Date(card.createdAt).toLocaleDateString()}</p>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div style={{ padding: '20px' }}>
|
|
89
|
+
<h1>Colorful Dashboard</h1>
|
|
90
|
+
|
|
91
|
+
{/* Pagination Info */}
|
|
92
|
+
{pagination && (
|
|
93
|
+
<div style={{ marginBottom: '16px' }}>
|
|
94
|
+
<p>
|
|
95
|
+
Showing {items.length} of {pagination.total} cards
|
|
96
|
+
{isLoading && ' • Loading...'}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{/* LazyList with virtual scrolling */}
|
|
102
|
+
<LazyList
|
|
103
|
+
items={items}
|
|
104
|
+
itemHeight={200}
|
|
105
|
+
viewportHeight={600}
|
|
106
|
+
fetchMore={fetchMore}
|
|
107
|
+
renderItem={renderCard}
|
|
108
|
+
bufferSize={5}
|
|
109
|
+
overscan={2}
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
{/* Load More Button (optional fallback) */}
|
|
113
|
+
{!hasMore && (
|
|
114
|
+
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
115
|
+
<p>No more cards to load</p>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default ColorfulDashboard;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend Pagination Helper for lazy-render
|
|
3
|
+
* Solves the 1 million cards performance issue
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PaginationParams {
|
|
7
|
+
page: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
cursor?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PaginatedResponse<T> {
|
|
13
|
+
data: T[];
|
|
14
|
+
pagination: {
|
|
15
|
+
page: number;
|
|
16
|
+
limit: number;
|
|
17
|
+
total: number;
|
|
18
|
+
totalPages: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
nextCursor?: string;
|
|
21
|
+
prevCursor?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calculate pagination metadata
|
|
27
|
+
*/
|
|
28
|
+
export function calculatePagination<T>(
|
|
29
|
+
items: T[],
|
|
30
|
+
page: number,
|
|
31
|
+
limit: number,
|
|
32
|
+
total: number
|
|
33
|
+
): PaginatedResponse<T> {
|
|
34
|
+
const totalPages = Math.ceil(total / limit);
|
|
35
|
+
const hasMore = page < totalPages;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
data: items,
|
|
39
|
+
pagination: {
|
|
40
|
+
page,
|
|
41
|
+
limit,
|
|
42
|
+
total,
|
|
43
|
+
totalPages,
|
|
44
|
+
hasMore,
|
|
45
|
+
nextCursor: hasMore ? String(page + 1) : undefined,
|
|
46
|
+
prevCursor: page > 1 ? String(page - 1) : undefined
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate pagination parameters
|
|
53
|
+
*/
|
|
54
|
+
export function validatePaginationParams(params: PaginationParams): {
|
|
55
|
+
page: number;
|
|
56
|
+
limit: number;
|
|
57
|
+
errors: string[];
|
|
58
|
+
} {
|
|
59
|
+
const errors: string[] = [];
|
|
60
|
+
|
|
61
|
+
// Validate page
|
|
62
|
+
const page = parseInt(String(params.page)) || 1;
|
|
63
|
+
if (page < 1) {
|
|
64
|
+
errors.push('Page must be greater than 0');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate limit
|
|
68
|
+
const limit = parseInt(String(params.limit)) || 50;
|
|
69
|
+
if (limit < 1) {
|
|
70
|
+
errors.push('Limit must be greater than 0');
|
|
71
|
+
}
|
|
72
|
+
if (limit > 1000) {
|
|
73
|
+
errors.push('Limit cannot exceed 1000 items per page');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
page: page < 1 ? 1 : page,
|
|
78
|
+
limit: limit < 1 ? 50 : limit > 1000 ? 1000 : limit,
|
|
79
|
+
errors
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Express.js middleware for pagination
|
|
85
|
+
*/
|
|
86
|
+
export function paginationMiddleware(
|
|
87
|
+
req: any,
|
|
88
|
+
res: any,
|
|
89
|
+
next: () => void
|
|
90
|
+
) {
|
|
91
|
+
const { page, limit, cursor } = req.query;
|
|
92
|
+
|
|
93
|
+
const validated = validatePaginationParams({
|
|
94
|
+
page: parseInt(String(page)) || 1,
|
|
95
|
+
limit: parseInt(String(limit)) || 50,
|
|
96
|
+
cursor
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (validated.errors.length > 0) {
|
|
100
|
+
return res.status(400).json({
|
|
101
|
+
error: 'Invalid pagination parameters',
|
|
102
|
+
details: validated.errors
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
req.pagination = {
|
|
107
|
+
page: validated.page,
|
|
108
|
+
limit: validated.limit,
|
|
109
|
+
cursor: validated.cursor
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
next();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Example Express route handler
|
|
117
|
+
*/
|
|
118
|
+
export function createPaginatedRoute<T>(
|
|
119
|
+
getDataFunction: (skip: number, limit: number) => Promise<{ items: T[]; total: number }>,
|
|
120
|
+
options: {
|
|
121
|
+
defaultLimit?: number;
|
|
122
|
+
maxLimit?: number;
|
|
123
|
+
} = {}
|
|
124
|
+
) {
|
|
125
|
+
const defaultLimit = options.defaultLimit || 50;
|
|
126
|
+
const maxLimit = options.maxLimit || 1000;
|
|
127
|
+
|
|
128
|
+
return async (req: any, res: any) => {
|
|
129
|
+
try {
|
|
130
|
+
const page = parseInt(req.query.page) || 1;
|
|
131
|
+
const limit = Math.min(
|
|
132
|
+
parseInt(req.query.limit) || defaultLimit,
|
|
133
|
+
maxLimit
|
|
134
|
+
);
|
|
135
|
+
const skip = (page - 1) * limit;
|
|
136
|
+
|
|
137
|
+
// Get data with pagination
|
|
138
|
+
const { items, total } = await getDataFunction(skip, limit);
|
|
139
|
+
|
|
140
|
+
// Return paginated response
|
|
141
|
+
const response = calculatePagination(items, page, limit, total);
|
|
142
|
+
|
|
143
|
+
res.json(response);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Pagination error:', error);
|
|
146
|
+
res.status(500).json({
|
|
147
|
+
error: 'Failed to fetch data',
|
|
148
|
+
message: error instanceof Error ? error.message : 'Unknown error'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default {
|
|
155
|
+
calculatePagination,
|
|
156
|
+
validatePaginationParams,
|
|
157
|
+
paginationMiddleware,
|
|
158
|
+
createPaginatedRoute
|
|
159
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazy-render-virtual-scroll",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A framework-agnostic virtual scrolling and lazy rendering solution",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -31,12 +31,17 @@
|
|
|
31
31
|
"import": "./dist/esm/svelte/index.js",
|
|
32
32
|
"require": "./dist/cjs/svelte/index.js",
|
|
33
33
|
"types": "./dist/svelte/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./backend-helpers": {
|
|
36
|
+
"import": "./backend-helpers/pagination.ts",
|
|
37
|
+
"require": "./backend-helpers/pagination.ts"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"files": [
|
|
37
41
|
"dist/",
|
|
38
42
|
"README.md",
|
|
39
|
-
"LICENSE"
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"backend-helpers/"
|
|
40
45
|
],
|
|
41
46
|
"scripts": {
|
|
42
47
|
"build": "rollup -c",
|