admin管理员组

文章数量:1310519

I'm building an emoji picker and the most demanding task is creating ~1500 DOM elements for each emoji which blocks/makes the page unresponsive for about 500-700ms.

I've debugged this and it seems like the creation of DOM elements is what blocks the rest of JS execution:

function load_emojis(arr){
  var $emojis = [];
  # arr.length = 1500
  _.each(arr, function(){
    $emojis.push($('<span/>').append($('<img/>')));
  });

  $('.emojis').html($emojis);
}

is there a way to execute this whole thing asynchronously/in another thread so it doesn't block the JS following it?

I've tried to put it inside setTimeout but that still seems to be executed in the same thread thus still blocking JS execution.

I'm building an emoji picker and the most demanding task is creating ~1500 DOM elements for each emoji which blocks/makes the page unresponsive for about 500-700ms.

I've debugged this and it seems like the creation of DOM elements is what blocks the rest of JS execution:

function load_emojis(arr){
  var $emojis = [];
  # arr.length = 1500
  _.each(arr, function(){
    $emojis.push($('<span/>').append($('<img/>')));
  });

  $('.emojis').html($emojis);
}

is there a way to execute this whole thing asynchronously/in another thread so it doesn't block the JS following it?

I've tried to put it inside setTimeout but that still seems to be executed in the same thread thus still blocking JS execution.

Share Improve this question asked Jun 21, 2017 at 19:38 CodeOverloadCodeOverload 48.6k56 gold badges133 silver badges223 bronze badges 8
  • You want to set timeout on each append and you want to appended them to a parent that is hidden. Once all elements are added. Unhide the parent. – Darkrum Commented Jun 21, 2017 at 19:42
  • 1 any chance you could do the creation server side? – Hopeless Commented Jun 21, 2017 at 19:43
  • Why don't you do anything with the values stored in arr? It seems like this code is not plete... – trincot Commented Jun 21, 2017 at 19:45
  • @trincot yeah i don't understand it either to me it looks like hes just creating empty <spans> with a empty <img> – Darkrum Commented Jun 21, 2017 at 19:47
  • I'm not sure if jQuery is doing this under the hood, but I would expect using document fragments to give you a little bit of a boost as apposed to forcing a DOM reflow with every addition of a new emoji. – zero298 Commented Jun 21, 2017 at 19:51
 |  Show 3 more ments

5 Answers 5

Reset to default 5

JavaScript isn't threaded; it's like most UI libraries in that it does everything in a single thread with an event loop; async behaviors may do work in background threads (invisible to the JS programmer; they don't explicitly manage or even see threads), but the results are always delivered to the single foreground thread for processing.

Element rendering isn't actually done until your JS code finishes, and control returns to the event loop; if you're rendering too many things, the delay occurs when the browser needs to draw it, and there is not much you can do to help. The most you could conceivably do is reduce the parsing overhead by explicitly creating elements rather than passing a wad of text for parsing, but even so, your options are limited.

Things that might help, depending on browser, phase of moon, etc., include:

  1. Create and populate a single parent element that's not part of the DOM at all, then add that single parent to the DOM when it's finished populating (can avoid work involved in maintaining DOM structure)
  2. Create a template of the structure you'll add repeatedly, then use cloneNode(True) to copy that template, and fill in the small differences after; this avoids the work of parsing X node trees when it's really the same node tree repeated a number of times with a few attribute tweaks.
  3. If the images are larger than fit in the viewport, only insert/render the initially visible elements, adding the others in the background as the user scrolls (possibly also adding them proactively, but in small enough numbers and with a sufficient setTimeout/setInterval window between them that any given insertion doesn't take very long, and the UI stays responsive).
  4. A big one for "array of many images" is to stop using 1500+ images, and instead use a single monolithic image. You then either:

    a. Use the monolithic image at a given fixed offset and size repeatedly via CSS (image is depressed and rendered once, and views of that image are mapped at different offsets repeatedly or...

    b. Use the map/area tags to insert the image only once, but make clicks behave differently on each part of the image (reduces the DOM layout work to a single image; all the other DOM elements must exist in the tree, but don't need to be rendered)

Here is a function that will divide the work into chunks:

function load_emojis(arr){
    var chunk = 20;
    $('.emojis').html(''); // clear before start, just to be sure
    (function loop(i) {
        if (i >= arr.length) return; // all done
        var $emojis = [];
        $.each(arr.slice(i, i+chunk), function(){
            $emojis.push($('<span/>').append($('<img/>')));
        });
        $('.emojis').append($emojis);
        setTimeout(loop.bind(null, i+chunk));
    })(0);
}

This will do a setTimeout for every 20 next items of your array.

Obviously the total time to plete will be longer, but other JS and user events can happen during the many little pauses.

I left out the second argument of setTimeout since the default (0) is enough to yield to other tasks in the event queue.

Also, I found your use of html() a bit odd, since the documentation allows the argument to be a string or a function, but you provided it an array of jQuery elements... In that case append() would be the function to use.

Play with the chunk size so to find the ideal size. It probably would be bigger than just 20.

My first solution isn't working, it only defer the problem. Your best shot is to optimize this functions a lot to perform it real quick.

function load_emojis(arr) {
  var emojis = new Array(arr.length);
  var emojisDiv = document.querySelector('.emojis');
  for (var i = 0; i < arr.length; i++) {
    emojis[i] = '<span><img src="" alt=""/></span>';
  }
  emojisDiv.innerHTML = emojis.join('');
}

NOT WORKING : There is one nice solution to this, but it's not a cross browser one, and only works in chrome firefox and opera : using window.requestIdleCallback.

function load_emojis(arr) {
  window.requestIdleCallback(function() {
     var $emojis = [];
     # arr.length = 1500
     _.each(arr, function(){
       $emojis.push($('<span/>').append($('<img/>')));
     });

     $('.emojis').html($emojis);
  });
}

see https://developer.mozilla/en-US/docs/Web/API/Window/requestIdleCallback

Here is a polyfill: https://github./PixelsCommander/requestIdleCallback-polyfill

Javascript is single threaded, so the short answer is no. So setTimeout will execute a function in the same thread, but will allow your code to keep running until the timeout has pleted (at which point it will block). For more on the event loop, MDN has a great explanation.

That being said, browsers implement the Web Worker API, which allow you to spin up separate processes in which to run JS code concurrently with your main process. So you can achieve some degree of multithreading in the browser. Unfortunately, workers do not have the same API access as your main process, and worker processes cannot render directly to the DOM, as explained in this post.

There's only one thread where you can perform DOM operations. You can perform non-DOM stuff in a separate thread with WebWorkers, but that obviously wouldn't work since you're adding to the DOM.

You're conducting 1501 DOM operations at once, each DOM operation is very costly. You can easily reduce it to 1 DOM operation:

function load_emojis(arr){
  var $emojis = [];
  # arr.length = 1500
  _.each(arr, function(){
    $emojis.push('<span><img src="..."/></span>');
  });

  $('.emojis').html($emojis.join(''));
}

This should be an order of magnitude slower than a single simple DOM operation, but that's much better than being 1500x slower. You should put in the src before it's added to the DOM, since you can't easily address the individual images anymore.

本文标签: javascriptCan you create thousands of DOM elements asynchronouslyStack Overflow