← JS

Virtual Scroll

Виртуальный скролл — рендерит только видимые элементы списка независимо от его размера. 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