单个页面上有1000个DOM元素

时间:2014-09-18 10:43:28

标签: javascript html html5 dom html5-canvas

对于一个大"文本地图的项目" BigPicture,我需要有超过1000个文本输入。 当您点击+拖动时,您可以" pan"显示的区域。

但性能非常差(在Firefox和Chrome上都有):渲染1000多个DOM元素并不快。

当然,另一个具有更好性能的解决方案是:处理<canvas>,将文本渲染为位图,每次我们要编辑文本时,让我们展示一个独特的DOM <textarea>,编辑完成后消失,文字再次呈现为位图... 工作正常(我目前正在朝着这个方向努力)但它还需要更多代码,以便在画布上提供编辑。

问题:是否有可能提高在HTML网页上呈现1000多个DOM元素的性能,以便我根本不需要使用<canvas> < / p>

或者在使用1000多个DOM元素平移页面时,不可能获得良好的性能吗?

enter image description here


注意:

1)在这里的演示中,我使用<span contendteditable="true">,因为我想要多行输入+自动调整大小,但渲染性能与标准<textarea>相同。*

2)作为参考,这就是我创建1000个文本元素的方法。

for (i=0; i < 1000; i++)
{
  var blax = (Math.random()-0.5)*3000;
  var blay = (Math.random()-0.5)*3000;
  var tb = document.createElement('span');
  $(tb).data("x", blax / $(window).width());
  $(tb).data("y", blay / $(window).height());
  $(tb).data("size", 20 * currentzoom);
  tb.contentEditable = true;
  tb.style.fontFamily = 'arial';
  tb.style.fontSize = '20px';
  tb.style.position  = 'absolute';
  tb.style.top = blay + 'px';
  tb.style.left = blax + 'px';
  tb.innerHTML="newtext";
  document.body.appendChild(tb);
}

4 个答案:

答案 0 :(得分:5)

对于类似这样的东西,你可以使用文档片段,这些是不属于实际DOM树的DOM节点(更多信息可以在这里https://developer.mozilla.org/en-US/docs/Web/API/document.createDocumentFragment找到),所以你可以做所有的设置片段然后附加片段,这片段只会导致一个重新流而不是1000个。

所以这是一个例子 - http://jsfiddle.net/leighking2/awzoz7bj/ - 快速检查运行时间大约需要60-70毫秒才能运行

var currentzoom = 1;
var docFragment = document.createDocumentFragment();
var start = new Date();
for (i=0; i < 1000; i++)
{
  var blax = (Math.random()-0.5)*3000;
  var blay = (Math.random()-0.5)*3000;
  var tb = document.createElement('span');
  $(tb).data("x", blax / $(window).width());
  $(tb).data("y", blay / $(window).height());
  $(tb).data("size", 20 * currentzoom);
  tb.contentEditable = true;
  tb.style.fontFamily = 'arial';
  tb.style.fontSize = '20px';
  tb.style.position  = 'absolute';
  tb.style.top = blay + 'px';
  tb.style.left = blax + 'px';
  tb.innerHTML="newtext";
  docFragment.appendChild(tb);
}

document.body.appendChild(docFragment);

var end = new Date();
console.log(end-start)

与原始版本相比,花费了大约645毫秒来运行http://jsfiddle.net/leighking2/896pusex/

更新因此,为了再次提高拖动速度,请将个别编辑保留在DOM之外,以避免每次拖动鼠标时重复1000次的费用

所以这是使用jquery的detach()方法的一种方法(例如http://jsfiddle.net/sf72ubdt/)。这将删除DOM中的元素,但是会将它们的所有属性提供给您,以便您可以操作它们并在以后重新插入它们

redraw = function(resize) {
    //detach spans
    var spans = $("span").detach();
    //now loop other them, because they are no longer attached to the DOM any changes are
    //not going to cause a reflow of the page
    $(spans).each(function(index) {
        var newx = Math.floor(($(this).data("x") - currentx) / currentzoom * $(window).width());
        var newy = Math.floor(($(this).data("y") - currenty) / currentzoom * $(window).height());

        if (resize) {
            displaysize = Math.floor($(this).data("size") / currentzoom);
            if (displaysize) {
                $(this).css({
                    fontSize: displaysize
                });
                $(this).show();
            } else
                $(this).hide();
        }
        //changed this from offset as I was getting a weird dispersing effect around the mouse
        // also can no longer test for visible but i assume you want to move them all anyway.
        $(this).css({
            top: newy + 'px',
            left: newx + 'px'
        });
    });
    //reattach to the body
    $("body").append(spans);

};

更新2 -

因此,为了获得更多性能,您可以缓存窗口宽度和高度,使用vanilla for循环,使用vanilla js来更改跨度的css。现在每次重绘(在chrome上)需要大约30-45毫秒(http://jsfiddle.net/leighking2/orpupsge/),相比我上面的更新,它看到它们大约80-100毫秒(http://jsfiddle.net/leighking2/b68r2xeu/

所以这里是更新的重绘

redraw = function (resize) {
    var spans = $("span").detach();
    var width = $(window).width();
    var height = $(window).height();

    for (var i = spans.length; i--;) {
        var span = $(spans[i]);
        var newx = Math.floor((span.data("x") - currentx) / currentzoom * width);
        var newy = Math.floor((span.data("y") - currenty) / currentzoom * height);
        if (resize) {
            displaysize = Math.floor(span.data("size") / currentzoom);
            if (displaysize) {
                span.css({
                    fontSize: displaysize
                });
                span.show();
            } else span.hide();
        }


        spans[i].style.top = newy + 'px',
        spans[i].style.left = newx + 'px'
    }

    $("body").append(spans);
};

SNIPPET示例 -

var currentzoom = 1;
var docFragment = document.createDocumentFragment();
var start = new Date();
var positions = []
var end = new Date();
console.log(end - start);

var currentx = 0.0,
currenty = 0.0,
currentzoom = 1.0,
xold = 0,
yold = 0,
button = false;

for (i = 0; i < 1000; i++) {
var blax = (Math.random() - 0.5) * 3000;
var blay = (Math.random() - 0.5) * 3000;

var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML = "newtext";
docFragment.appendChild(tb);
}
document.body.appendChild(docFragment);

document.body.onclick = function (e) {
if (e.target.nodeName == 'SPAN') {
    return;
}
var tb = document.createElement('span');

$(tb).data("x", currentx + e.clientX / $(window).width() * currentzoom);
$(tb).data("y", currenty + e.clientY / $(window).height() * currentzoom);
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.backgroundColor = 'transparent';
tb.style.position = 'absolute';
tb.style.top = e.clientY + 'px';
tb.style.left = e.clientX + 'px';
document.body.appendChild(tb);
tb.focus();
};

document.body.onmousedown = function (e) {
button = true;
xold = e.clientX;
yold = e.clientY;
};

document.body.onmouseup = function (e) {
button = false;
};

redraw = function (resize) {
var start = new Date();
var spans = $("span").detach();
var width = $(window).width();
var height = $(window).height();

for (var i = spans.length; i--;) {
    var span = $(spans[i]);
    var newx = Math.floor((span.data("x") - currentx) / currentzoom * width);
    var newy = Math.floor((span.data("y") - currenty) / currentzoom * height);
    if (resize) {
        displaysize = Math.floor(span.data("size") / currentzoom);
        if (displaysize) {
            span.css({
                fontSize: displaysize
            });
            span.show();
        } else span.hide();
    }


    spans[i].style.top = newy + 'px',
    spans[i].style.left = newx + 'px'
}

$("body").append(spans);
var end = new Date();
console.log(end - start);
};

document.body.onmousemove = function (e) {
if (button) {
    currentx += (xold - e.clientX) / $(window).width() * currentzoom;
    currenty += (yold - e.clientY) / $(window).height() * currentzoom;

    xold = e.clientX;
    yold = e.clientY;

    redraw(false);
}
};

$(function () {
$('body').on('mousedown', 'span', function (event) {
    if (event.which == 3) {
        $(this).remove()
    }
})
});

zoomcoef = function (coef) {
middlex = currentx + currentzoom / 2
middley = currenty + currentzoom / 2
currentzoom *= coef
currentx = middlex - currentzoom / 2
currenty = middley - currentzoom / 2
redraw(true)
}

window.onkeydown = function (event) {
if (event.ctrlKey && event.keyCode == 61) {
    zoomcoef(1 / 1.732);
    event.preventDefault();
}
if (event.ctrlKey && event.keyCode == 169) {
    zoomcoef(1.732);
    event.preventDefault();
}
if (event.ctrlKey && event.keyCode == 48) {
    zoomonwidget(1 / 1.732);
    event.preventDefault();
}
};
  html, body {
      height: 100%;
      width: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>

答案 1 :(得分:1)

恕我直言,我会按照你目前的想法来最大化表现。

原因:1000多个DOM元素将始终限制性能。

是的,编码稍微多一些,但你的表现要好得多。

  • 创建一个包含所有1000个文本的大型屏幕外画布。

  • 使用context.textMeasure计算相对于图像的所有1000个文本的边界框。

  • 保存有关对象中每个文本的信息

    var texts=[];
    var texts[0]={ text:'text#0',  x:100, y:100, width:35, height:20 }
    

    ...

  • context.drawImage画布上的图像使用offset-X来“平移”图像。这样,您只有1个canvas元素而不是1000个文本元素。

  • 在mousedown处理程序中,检查鼠标位置是否在任何文本的边界框内。

  • 如果在文本边界框内单击鼠标,则绝对将输入类型文本直接放在画布上的文本上。这样,您只需要1个输入元素,可以重复使用1000个文本中的任何一个。

  • 使用input元素的功能让用户编辑文本。 canvas元素没有原生文本编辑功能,因此不要通过编写画布文本编辑来“重新创建轮子”。

  • 用户完成编辑后,重新计算新编辑文本的边界框并将其保存到文本对象中。

  • 使用新编辑的文本重绘包含所有1000个文本的屏幕外画布,并将其绘制到屏幕画布上。

  • 平移:如果用户拖动屏幕画布,请将屏幕外画布绘制到屏幕画布上,其偏移量等于用户拖动鼠标的距离。平移几乎是瞬间的,因为将屏幕外画布绘制到屏幕画布上 - 视口比移动1000个DOM输入元素要快得多

[添加:编辑和平移的完整示例]

**在全屏模式下观看最佳**

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;

var texts=[];
var fontSize=12;
var fontFace='arial';

var tcanvas=document.createElement("canvas");
var tctx=tcanvas.getContext("2d");
tctx.font=fontSize+'px '+fontFace;
tcanvas.width=3000;
tcanvas.height=3000;

var randomMaxX=tcanvas.width-40;
var randomMaxY=tcanvas.height-20;
var panX=-tcanvas.width/2;
var panY=-tcanvas.height/2;
var isDown=false;
var mx,my;       

var textCount=1000;
for(var i=0;i<textCount;i++){
  var text=(i+1000);
  texts.push({
    text:text,
    x:parseInt(Math.random()*randomMaxX),
    y:parseInt(Math.random()*randomMaxY)+20,
    width:ctx.measureText(text).width,
    height:fontSize+2,
  });
}

var $textbox=$('#textbox');
$textbox.css('left',-200);
$textbox.blur(function(){
  $textbox.css('left',-200);
  var t=texts[$textbox.textsIndex]
  t.text=$(this).val();
  t.width=ctx.measureText(t.text).width;
  textsToImage();    
});


textsToImage();

$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});


// create one image from all texts[]
function textsToImage(){
  tctx.clearRect(0,0,tcanvas.width,tcanvas.height);
  for(var i=0;i<textCount;i++){
    var t=texts[i];
    tctx.fillText(t.text,t.x,t.y)
    tctx.strokeRect(t.x,t.y-fontSize,t.width,t.height);
  }
  redraw();
}

function redraw(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.drawImage(tcanvas,panX,panY);
}

function handleMouseDown(e){
  e.preventDefault();
  e.stopPropagation();

  mx=parseInt(e.clientX-offsetX);
  my=parseInt(e.clientY-offsetY);

  // is the mouse over a text?
  var hit=false;
  var x=mx-panX;
  var y=my-panY;
  for(var i=0;i<texts.length;i++){
    var t=texts[i];
    if(x>=t.x && x<=t.x+t.width && y>=t.y-fontSize && y<=t.y-fontSize+t.height){
      $textbox.textsIndex=i;
      $textbox.css({'width':t.width+5, 'left':t.x+panX, 'top':t.y+panY-fontSize});
      $textbox.val(t.text);
      $textbox.focus();
      hit=true;
      break;
    }
  }

  // mouse is not over any text, so start panning
  if(!hit){isDown=true;}
}

function handleMouseUpOut(e){
  e.preventDefault();
  e.stopPropagation();
  isDown=false;
}

function handleMouseMove(e){
  if(!isDown){return;}
  e.preventDefault();
  e.stopPropagation();

  var mouseX=parseInt(e.clientX-offsetX);
  var mouseY=parseInt(e.clientY-offsetY);
  panX+=mouseX-mx;
  panY+=mouseY-my;
  mx=mouseX;
  my=mouseY;
  redraw();
}
body{ background-color: ivory; padding:10px; }
#wrapper{position:relative; border:1px solid blue; width:600px; height:600px;}
#textbox{position:absolute;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click on #box to edit.<br>Tab to save changes.<br>Drag on non-text.</h4><br>
<div id=wrapper>
  <input type=text id=textbox>
  <canvas id="canvas" width=600 height=600></canvas>
</div>
<button></button>

答案 2 :(得分:1)

@Shmiddty给出了一个解决方案,它比以前的所有尝试都快得多:所有元素都应该被包装,只需要移动包装器(而不是移动每个元素):

http://jsfiddle.net/qhskacsw/

即使有超过1000个DOM元素,它也能顺畅而快速地运行。


var container = document.createElement("div"),
    wrapper = document.createElement("div"),
    dragging = false,
    offset = {x:0, y:0},
    previous = {x: 0, y:0};

container.style.position = "absolute";
wrapper.style.position = "relative";
container.appendChild(wrapper);
document.body.appendChild(container);

for (var i = 1000, span; i--;){
    span = document.createElement("span");
    span.textContent = "banana";
    span.style.position = "absolute";
    span.style.top = (Math.random() * 3000 - 1000 | 0) + 'px';
    span.style.left = (Math.random() * 3000 - 1000 | 0) + 'px';

    wrapper.appendChild(span);
}


// Don't attach events like this. 
// I'm only doing it for this proof of concept.

window.ondragstart = function(e){
    e.preventDefault();
}

window.onmousedown = function(e){
    dragging = true;
    previous = {x: e.pageX, y: e.pageY};
}

window.onmousemove = function(e){
    if (dragging){
        offset.x += e.pageX - previous.x;
        offset.y += e.pageY - previous.y;
        previous = {x: e.pageX, y: e.pageY};

        container.style.top = offset.y + 'px';
        container.style.left = offset.x + 'px';
    }
}

window.onmouseup = function(){
    dragging = false;
}

答案 3 :(得分:0)

我只是运行几个测试,似乎用CSS转换移动绝对定位(位置:绝对;)DOM元素(div):translate比通过Canvas移动更快(大约30%)。但我使用CreateJS框架作为canvas作业,所以我的结果可能不适用于其他用例。