angular-ellipsis icon indicating copy to clipboard operation
angular-ellipsis copied to clipboard

Performance is really bad at ng-repeat.

Open martingg88 opened this issue 10 years ago • 13 comments

There is the worst performance if I implement it in ng-repeat.. Any help would be more appreciated.

martingg88 avatar Sep 11 '14 19:09 martingg88

Hi martingg88

It could be a bit later... But you can try my improvement if you still need it. On my page, there are 10 very looooooooooong text elements in ng-repeat with data-ellipsis. Running with the current code, re-sizing page takes 3~10 seconds to respond; running with my improved code, it's real-time.


function buildEllipsis() {
  if (typeof(scope.ngBind) !== 'undefined') {
    var bindArray = scope.ngBind.split(" "),
      ellipsisSymbol = (typeof(attributes.ellipsisSymbol) !== 'undefined') ? attributes.ellipsisSymbol : '…',
      appendString = (typeof(scope.ellipsisAppend) !== 'undefined' && scope.ellipsisAppend !== '') ? ellipsisSymbol + '<span>' + scope.ellipsisAppend + '</span>' : ellipsisSymbol;

    attributes.isTruncated = false;
    element.html(scope.ngBind);

    // refine the algorithm to improve the performance significantly
    //                            [email protected] 20150228
    var desiredHeight = element[0].clientHeight;
    var actualHeight = element[0].scrollHeight;
    if (actualHeight > desiredHeight) {
      // PERFORMANCE IMPROVEMENT: calc the proper size by heights
      var size = Math.floor(bindArray.length *
                           desiredHeight / actualHeight);
      var text = bindArray.slice(0, size).join(' ');
      element.html(text + appendString);
      while (isOverflowed(element) && size > 0) {
        --size;
        // PERFORMANCE IMPROVEMENT: use String.substr rather than Array.join
        text = text.substr(0, text.length - bindArray[size].length - 1);
        element.html(text + appendString);
      }
    }
  }
}

HuangJian avatar Mar 01 '15 13:03 HuangJian

Tested in production environment. Huge improvement. Thank you!

MarcoDeJong avatar Mar 05 '15 08:03 MarcoDeJong

@miraclemarc, please try this updated version. It places the ellipsis more accurate.

function buildEllipsis() {
  if (typeof(scope.ngBind) !== 'undefined') {
    var str = scope.ngBind,
      ellipsisSymbol = (typeof(attributes.ellipsisSymbol) !== 'undefined') ? attributes.ellipsisSymbol : '&hellip;',
      appendString = (typeof(scope.ellipsisAppend) !== 'undefined' && scope.ellipsisAppend !== '') ? ellipsisSymbol + '<span>' + scope.ellipsisAppend + '</span>' : ellipsisSymbol;

    attributes.isTruncated = false;
    element.html(scope.ngBind);

    var desiredHeight = element[0].clientHeight;
    var actualHeight = element[0].scrollHeight;
    if (actualHeight > desiredHeight) {
      attributes.isTruncated = true;

      var spliter = ' ';
      var lineHeight = parseFloat(element.css('line-height'));

      // the max possible characters that might not overflow the desired height
      var max = Math.ceil(str.length * (desiredHeight + lineHeight) / actualHeight);

      // the min characters that must not overflow the desired height
      var min = Math.floor(str.length * (desiredHeight - lineHeight) / actualHeight);
      min = str.substr(0, min).lastIndexOf(spliter);

      // set with the max possible size, then reduce its size word by word
      var size = str.indexOf(spliter, max);
      if (size < 0) {
        // no spliter after max
        size = max;
      }

      var text = str.substr(0, size).trim();
      var arr = str.substr(min, size - min).trim().split(spliter);
      var idx = arr.length;
      element.html(text + appendString);
      while (isOverflowed(element) && idx >= 0) {
        --idx;
        text = text.substr(0, text.length - arr[idx].length - 1);
        element.html(text + appendString);
      }
    }
  }
}

HuangJian avatar Mar 05 '15 09:03 HuangJian

Thank you! I will test your latest additions

Update: Tested last snippet. But your first one actually works better for me.

MarcoDeJong avatar Mar 09 '15 07:03 MarcoDeJong

is this improvement going to get committed ?

a-c-m avatar Mar 20 '15 13:03 a-c-m

@miraclemarc

yep, the first version is better in performance, but it could place the ellipsis incorrectly. Like:

bla bla bla... (looooooooooong space in the last line)

or even

bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla... (empty area could fill one or more lines) (empty area could fill one or more lines)

HuangJian avatar Mar 20 '15 14:03 HuangJian

Just curious instead of substracting words from the end, which could seriously hurt performance when the text is long, how about we use an idea similar to "binary search"? The key here is to find the "boundary position" where the text just overflows.

tangentlin avatar Nov 10 '15 18:11 tangentlin

Here is a faster version based on the first version by @HuangJian , it uses binary search to find the "boundary position", along with other performance enhancement such as using .innerHTML instead $.html(), the improvement is more noticeable.

var domElement = element[0];

function getEmptySpaceLocations(text) {
    var spaceIndices = [];
    var re = /\s+/gm;
    var match;
    while ((match = re.exec(text)) != null) {
        spaceIndices.push(match.index);
    }
    return spaceIndices;
}
function getSubText(fullText, spaceIndices, index) {
    return fullText.substr(0, spaceIndices[index]);
}
function isSubTextOverflow(fullText, spaceIndices, index, appendString) {
    var text = getSubText(fullText, spaceIndices, index);
    domElement.innerHTML = text + appendString;
    return isOverflown(element);
}
function buildEllipsis() {
    if (typeof (scope.ngBind) !== 'undefined') {
        var text = scope.ngBind;
        var spaceIndices = getEmptySpaceLocations(text);
        attributes.isTruncated = false;
        element.html(text);
        var desiredHeight = element[0].clientHeight;
        var actualHeight = element[0].scrollHeight;
        if (actualHeight > desiredHeight) {
            var totalSpaceLocations = spaceIndices.length;
            var begin = 0;
            var end = totalSpaceLocations - 1;
            var lastOverflown = true;
            var currentIndex;
            var currentOverflown = true;
            var notFound = true;
            var seekedTimes = 0;
            var ellipsisSymbol = (typeof (attributes.ellipsisSymbol) !== 'undefined') ? attributes.ellipsisSymbol : '&hellip;';
            var appendString = (typeof (scope.ellipsisAppend) !== 'undefined' && scope.ellipsisAppend !== '') ? ellipsisSymbol + '<span>' + scope.ellipsisAppend + '</span>' : ellipsisSymbol;
            while (notFound) {
                currentIndex = begin + ((end - begin) >> 1);
                currentOverflown = isSubTextOverflow(text, spaceIndices, currentIndex, appendString);
                seekedTimes++;
                if ((currentOverflown != lastOverflown) && (end - begin) == 1) {
                    notFound = false;
                }
                else {
                    if (currentOverflown) {
                        end = currentIndex;
                    }
                    else {
                        begin = currentIndex;
                    }
                }
            }
            var truncatedText = getSubText(text, spaceIndices, currentIndex) + appendString;
            element.html(truncatedText);
            attributes.isTruncated = true;
            console.log('Seeked: ' + seekedTimes + ' Spaces: ' + totalSpaceLocations + ' Length: ' + text.length);
        }
    }
}
function isOverflown(thisElement) {
    return thisElement[0].scrollHeight > thisElement[0].clientHeight;
}

tangentlin avatar Nov 11 '15 00:11 tangentlin

@ MauMaGau Bump, could you please integrate this fix like they did in the educarlabs fork (which is less maintained). This fix is a huge performance upgrade when the text that has to be ellipsed is very long.

NinoFloris avatar Oct 06 '16 13:10 NinoFloris

Ok so I have put my money where my mouth is a few weeks after I asked for this here and integrated it myself. However I still haven't opened a PR, something that slipped... because life.

Do know I have a present for you all 🎁 and please do nag me about it if I still haven't done this a while after this comment ^_^

NinoFloris avatar Dec 21 '16 21:12 NinoFloris

nag :nail_care:

MarcoDeJong avatar Jan 03 '17 15:01 MarcoDeJong

nag 💅

eddieedease avatar Jan 09 '17 10:01 eddieedease

There you go! https://github.com/dibari/angular-ellipsis/pull/79

NinoFloris avatar Jan 16 '17 12:01 NinoFloris