Виртуальный скролл — рендерит только видимые элементы списка независимо от его размера. 10 000 элементов в DOM не тормозят — в реальности отрисовывается только ~20. Незаменим для больших таблиц, лент и списков.
(function(){
const root = document.createElement('div');
document.body.appendChild(root);
const container = root;
try {
const style = document.createElement('style');
style.textContent = '.vs-wrap{font-family:monospace;background:#0d1117;border-radius:12px;overflow:hidden;border:1px solid #30363d}'
+ '.vs-header{padding:12px 16px;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;background:#161b22}'
+ '.vs-title{color:#00ff9c;font-size:13px;font-weight:bold}'
+ '.vs-counter{font-size:11px;color:#8b949e}'
+ '.vs-viewport{height:320px;overflow-y:auto;position:relative;scrollbar-width:thin;scrollbar-color:#30363d #0d1117}'
+ '.vs-viewport::-webkit-scrollbar{width:6px}'
+ '.vs-viewport::-webkit-scrollbar-track{background:#0d1117}'
+ '.vs-viewport::-webkit-scrollbar-thumb{background:#30363d;border-radius:3px}'
+ '.vs-spacer{position:absolute;top:0;left:0;width:1px;pointer-events:none}'
+ '.vs-content{position:absolute;left:0;right:0}'
+ '.vs-item{display:flex;align-items:center;gap:12px;padding:0 16px;border-bottom:1px solid #21262d;cursor:pointer;transition:background 0.1s}'
+ '.vs-item:hover{background:#161b22}'
+ '.vs-item-index{font-size:11px;color:#484f58;width:40px;flex-shrink:0;text-align:right}'
+ '.vs-item-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}'
+ '.vs-item-text{flex:1;font-size:13px;color:#e6edf3}'
+ '.vs-item-badge{font-size:10px;padding:2px 8px;border-radius:20px;flex-shrink:0}'
+ '.vs-item-views{font-size:11px;color:#484f58}'
+ '.vs-footer{padding:10px 16px;border-top:1px solid #30363d;font-size:11px;color:#484f58;background:#161b22;display:flex;justify-content:space-between}';
document.head.appendChild(style);
var TOTAL = 10000;
var ITEM_H = 44;
var BUFFER = 3;
var categories = ['JS', 'HTML', 'CSS', 'PHP', 'App'];
var colors = ['#00ff9c', '#a78bfa', '#22d3ee', '#fb923c', '#f472b6'];
var names = [
'debounce', 'throttle', 'smoothScroll', 'copyText', 'formatDate',
'randomColor', 'shuffleArray', 'typewriter', 'rippleEffect', 'glitchText',
'Card Component', 'Nav Bar', 'Modal', 'Toast', 'Skeleton',
'Progress Bar', 'Avatar Group', 'Pricing Card', 'Stats Card', 'Profile Card'
];
var data = [];
for (var d = 0; d < TOTAL; d++) {
data.push({
id: d + 1,
name: names[d % names.length] + (d >= names.length ? ' v' + (Math.floor(d / names.length) + 1) : ''),
category: categories[d % categories.length],
color: colors[d % colors.length],
views: Math.floor(Math.random() * 9999)
});
}
var wrap = document.createElement('div');
wrap.className = 'vs-wrap';
var header = document.createElement('div');
header.className = 'vs-header';
var vsTitle = document.createElement('span');
vsTitle.className = 'vs-title';
vsTitle.textContent = '// Virtual Scroll';
var vsCounter = document.createElement('span');
vsCounter.className = 'vs-counter';
vsCounter.textContent = TOTAL.toLocaleString() + ' elements';
header.appendChild(vsTitle);
header.appendChild(vsCounter);
var viewport = document.createElement('div');
viewport.className = 'vs-viewport';
var spacer = document.createElement('div');
spacer.className = 'vs-spacer';
spacer.style.height = (TOTAL * ITEM_H) + 'px';
var content = document.createElement('div');
content.className = 'vs-content';
viewport.appendChild(spacer);
viewport.appendChild(content);
var footer = document.createElement('div');
footer.className = 'vs-footer';
var rangeEl = document.createElement('span');
rangeEl.textContent = 'Elements 1-20';
var domEl = document.createElement('span');
domEl.textContent = 'In DOM: 0 of ' + TOTAL.toLocaleString();
footer.appendChild(rangeEl);
footer.appendChild(domEl);
wrap.appendChild(header);
wrap.appendChild(viewport);
wrap.appendChild(footer);
container.appendChild(wrap);
function render() {
var scrollTop = viewport.scrollTop;
var viewH = viewport.clientHeight;
var startIdx = Math.max(0, Math.floor(scrollTop / ITEM_H) - BUFFER);
var endIdx = Math.min(TOTAL - 1, Math.ceil((scrollTop + viewH) / ITEM_H) + BUFFER);
content.style.top = (startIdx * ITEM_H) + 'px';
content.innerHTML = '';
for (var i = startIdx; i <= endIdx; i++) {
var item = data[i];
var el = document.createElement('div');
el.className = 'vs-item';
el.style.height = ITEM_H + 'px';
var idxEl = document.createElement('span');
idxEl.className = 'vs-item-index';
idxEl.textContent = '#' + item.id;
var dot = document.createElement('span');
dot.className = 'vs-item-dot';
dot.style.background = item.color;
var text = document.createElement('span');
text.className = 'vs-item-text';
text.textContent = item.name;
var badge = document.createElement('span');
badge.className = 'vs-item-badge';
badge.style.background = item.color + '22';
badge.style.color = item.color;
badge.textContent = item.category;
var views = document.createElement('span');
views.className = 'vs-item-views';
views.textContent = 'views: ' + item.views;
el.appendChild(idxEl);
el.appendChild(dot);
el.appendChild(text);
el.appendChild(badge);
el.appendChild(views);
content.appendChild(el);
}
rangeEl.textContent = 'Elements ' + (startIdx + 1) + '-' + Math.min(endIdx + 1, TOTAL);
domEl.textContent = 'In DOM: ' + (endIdx - startIdx + 1) + ' of ' + TOTAL.toLocaleString();
}
viewport.addEventListener('scroll', render);
render();
} catch(e){
console.error(e);
}
})();
👁 8