admin管理员组

文章数量:1315314

I built up a tree table structure for AngularJS (with Angular Material) some time ago.

My target was to make it work on large screens only (1280 and higher) but right now I want to update it and make it work on smaller devices (mostly tablets) without limiting data. Because of performance, I want to keep HTML as simple as possible (tree table can have 1000+ rows so creating more plicated HTML for the single row will elongate the time needed to append and render table row (rows are dynamic so it's not only about initial rendering)).

I came up with idea that I will keep the "fixed" part with the first cell which contains a name and scroll the second part which contains all metrics and will be scrolled synchronically.

Current HTML of single row:

<div class="tb-header layout-row">
  <div class="tb-title"><span>Name</span></div>
  <div class="tb-metrics">
     <div class="layout-row">
        <div class="tb-cell flex-10">812</div>
        <div class="tb-cell flex-7">621</div>
        <div class="tb-cell flex-4">76.5</div>
        <div class="tb-cell flex-7">289</div>
        <div class="tb-cell flex-4">46.5</div>
        <div class="tb-cell flex-7">308</div>
        <div class="tb-cell flex-4">49.6</div>
        <div class="tb-cell flex-7">390</div>
        <div class="tb-cell flex-4">48.0</div>
        <div class="tb-cell flex-7">190</div>
        <div class="tb-cell flex-4">23.4</div>
        <div class="tb-cell flex-7">0</div>
        <div class="tb-cell flex-4">0.0</div>
        <div class="tb-cell flex-8">6.4</div>
        <div class="tb-cell flex-8">0.0</div>
        <div class="tb-cell flex-8"></div>
     </div>
  </div>

My idea was to use touchmove event on parent container (wrapping the whole tree and bind as a directive) and check when touchmove starts over the metrics section then calculate the value which I should move metrics. And that part works fine. The problem starts when I want to apply the offset on the .tb-metrics > .

My first try was to use jQuery:

function moveMetrics( offset ) {
  var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0;
  $('.tb-metrics').children().css('transform', 'translateX(' + ofx + 'px)');

  /*...*/

}

Unfortunately, this solution is quite slow when the table contains more rows (I cannot cache rows because they are dynamic).

In my second attempt, a tried to avoid as much DOM manipulation as I can. To achieve that I decided to add <script> tag to dom which contains css which applies to .metrics > .layout-row.

Solution:

function moveMetrics( offset ) {
  var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0
    , style = $( '#tbMetricsVirtualScroll' )
    ;

  if ( !style.length ) {
    body.append( '<style id="tbMetricsVirtualScroll">.tb-metrics > * {transform: translateX(' + ofx + 'px)}</style>' );
    style = $( '#tbMetricsVirtualScroll' );
  } else {
    style.text( '.tb-metrics > * {transform: translateX(' + ofx + 'px)}' );
  }
  /*...*/
}

However, it doesn't seem to be much faster when the table contains a large number of rows. So it's not DOM manipulation but rendering/painting view seems to be the bottleneck here.

I tried to create some kind of virtual scroll but because tree structure is different for different sets of data and can have an "infinite" number of levels (each row can contain children rows in new ng-repeat) it's a really hard task.

I will appreciate any ideas about how I can improve performance in that situation without using the virtual scroll.

EDIT:

Screenshot of the Chrome timeline shows that most time of scrolling is consumed by rendering (I guess that it is because of plicated DOM structure)

EDIT 2:

I won't say that I achieved absolutely smooth scrolling, but I found a couple of things for significant performance improvement (some of them weren't obvious and the result is better than I expected after such small changes).

  • Simplify class selectors : .tb-header > .tb-metrics > .tb-cell is much slower than .tb-specific-cell (it seems that it take more time to parse more plicated selectors?)
  • remove opacity and box shadows from transformed elements
  • try to distribute transformed element to new layer (use css will-change and/or translateZ(0))

I built up a tree table structure for AngularJS (with Angular Material) some time ago.

My target was to make it work on large screens only (1280 and higher) but right now I want to update it and make it work on smaller devices (mostly tablets) without limiting data. Because of performance, I want to keep HTML as simple as possible (tree table can have 1000+ rows so creating more plicated HTML for the single row will elongate the time needed to append and render table row (rows are dynamic so it's not only about initial rendering)).

I came up with idea that I will keep the "fixed" part with the first cell which contains a name and scroll the second part which contains all metrics and will be scrolled synchronically.

Current HTML of single row:

<div class="tb-header layout-row">
  <div class="tb-title"><span>Name</span></div>
  <div class="tb-metrics">
     <div class="layout-row">
        <div class="tb-cell flex-10">812</div>
        <div class="tb-cell flex-7">621</div>
        <div class="tb-cell flex-4">76.5</div>
        <div class="tb-cell flex-7">289</div>
        <div class="tb-cell flex-4">46.5</div>
        <div class="tb-cell flex-7">308</div>
        <div class="tb-cell flex-4">49.6</div>
        <div class="tb-cell flex-7">390</div>
        <div class="tb-cell flex-4">48.0</div>
        <div class="tb-cell flex-7">190</div>
        <div class="tb-cell flex-4">23.4</div>
        <div class="tb-cell flex-7">0</div>
        <div class="tb-cell flex-4">0.0</div>
        <div class="tb-cell flex-8">6.4</div>
        <div class="tb-cell flex-8">0.0</div>
        <div class="tb-cell flex-8"></div>
     </div>
  </div>

My idea was to use touchmove event on parent container (wrapping the whole tree and bind as a directive) and check when touchmove starts over the metrics section then calculate the value which I should move metrics. And that part works fine. The problem starts when I want to apply the offset on the .tb-metrics > .

My first try was to use jQuery:

function moveMetrics( offset ) {
  var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0;
  $('.tb-metrics').children().css('transform', 'translateX(' + ofx + 'px)');

  /*...*/

}

Unfortunately, this solution is quite slow when the table contains more rows (I cannot cache rows because they are dynamic).

In my second attempt, a tried to avoid as much DOM manipulation as I can. To achieve that I decided to add <script> tag to dom which contains css which applies to .metrics > .layout-row.

Solution:

function moveMetrics( offset ) {
  var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0
    , style = $( '#tbMetricsVirtualScroll' )
    ;

  if ( !style.length ) {
    body.append( '<style id="tbMetricsVirtualScroll">.tb-metrics > * {transform: translateX(' + ofx + 'px)}</style>' );
    style = $( '#tbMetricsVirtualScroll' );
  } else {
    style.text( '.tb-metrics > * {transform: translateX(' + ofx + 'px)}' );
  }
  /*...*/
}

However, it doesn't seem to be much faster when the table contains a large number of rows. So it's not DOM manipulation but rendering/painting view seems to be the bottleneck here.

I tried to create some kind of virtual scroll but because tree structure is different for different sets of data and can have an "infinite" number of levels (each row can contain children rows in new ng-repeat) it's a really hard task.

I will appreciate any ideas about how I can improve performance in that situation without using the virtual scroll.

EDIT:

Screenshot of the Chrome timeline shows that most time of scrolling is consumed by rendering (I guess that it is because of plicated DOM structure)

EDIT 2:

I won't say that I achieved absolutely smooth scrolling, but I found a couple of things for significant performance improvement (some of them weren't obvious and the result is better than I expected after such small changes).

  • Simplify class selectors : .tb-header > .tb-metrics > .tb-cell is much slower than .tb-specific-cell (it seems that it take more time to parse more plicated selectors?)
  • remove opacity and box shadows from transformed elements
  • try to distribute transformed element to new layer (use css will-change and/or translateZ(0))
Share Improve this question edited May 4, 2021 at 13:59 Mo Asghari 2512 silver badges11 bronze badges asked Mar 21, 2016 at 10:54 LJ WadowskiLJ Wadowski 6,72811 gold badges48 silver badges77 bronze badges 8
  • 1 I would suggest looking into virtual repeat github./stackfull/angular-virtual-scroll – koningdavid Commented Mar 21, 2016 at 10:57
  • 1 you should use material design which is responsive and will automatically convert your large tables – Amit Sirohiya Commented Mar 21, 2016 at 11:00
  • @koningdavid I was trying to implement it, but the problem is that, I am using recurrent directive so every row can have it's own children rows (and another ng-repeat) so it makes it much more plicated – LJ Wadowski Commented Mar 21, 2016 at 11:02
  • One of the critical parts of smooth scrolling on mobile is to scroll the whole document instead of some distinct container with overflow: auto. It is hard to implement entirely smooth scrolling for a distinct container. Does it apply to your situation? Is whole document being scrolled or a distinct container? – Andrew Sklyarevsky Commented Aug 19, 2016 at 9:15
  • 1 If you have a lot of data, you likely want to have your say ~10 rows shown, but they are purely placeholders... when the user scrolls... you determine what they would have theoretically scrolled to, and drop in that data... into your placeholders, keeping the big set of data in memory only... and only render the visible elements when you need to. – scunliffe Commented Mar 10, 2018 at 1:09
 |  Show 3 more ments

3 Answers 3

Reset to default 1

I have had similar problems in the past with really big tables. So, leaving the UX side of things outside of the equation - in order to achieve what you are aiming for could be the following. Instead of keeping the first tables fixed and moving all the rest, maybe you could try the other way around. Aka, keep the table inside an overflown container to allow horizontal scrolling and move using translateY the first column table cells when scrolling vertically.

In the way I am describing, you can retrieve the max width of the first table cell (or even better, set a fixed width), absolutely position each cell on the left - careful though, the relative container should be OUTSIDE the overflown table, set a padding-left on the second table cell of each row, equal to the first table-cell and then, when scrolling vertically, add a negative translateY value. The selector then should be easy to access '.layout-row .table-cell:first-child'.

Also for minor optimizing, instead of using .css() try using .attr('style', value) instead. Also, I read that you have dynamic content and can't cache it, BUT, maybe you should consider caching once every time you manipulate the DOM. Cached selected elements are still better than calculating the selector in every scroll/touchmove event.

Last but not least, you could also add somekind of debouncing technique so that the scrolling does not occur on every scroll frame, but maybe once every 10 frames - it would still be invisible to the eye, or in any case better than bottle-necking the browser with all these repaint events.

I would suggest you use lazy loading. please note that you have 4 options in lazy loading: There are four mon ways of implementing the lazy load design pattern: lazy initialization; a virtual proxy; a ghost, and a value holder. Each has its own advantages and disadvantages (link).

Good luck

So I'm no expert myself but I do have some ideas that I think are worth mentioning. If I'm understanding correctly, the problem is that this project needs an increasing number of HTML elements, which would continually decrease performance

Here are 3 js concepts that I think would help you

*Dynamically creating the html element:

let div = document.createElement('div');

*Dynamically creating the attributes you're going to use for the elements, and dynamically being able to assign such attributes

function attributeSetter(atr){
   div.setAttribute('class', atr)
}

That way you can call the function wherever you need it, and give it whatever class name you want to give it dynamically. Furthermore, you can use the div element as many times as you want

final concept. You can also dynamically add pre-made classes, assuming that they're already pre-defined in css, like this.

div.classList.add("my-class-name")

One more concept that might e in handy, is de-structuring

let myClasses = ['flex-10', 'flex-7', 'flex-4', 'flex-9'] //etc
let [flex10, flex7, flex4, flex9] = myClasses;

This will turn all those strings into variables that you can easily iterate through and dynamically add them to your attributeSetter function, like this:

attributeSetter(flex10)

Wherever you need it. I don't know if that helps, or if that answered your question, but at least some ideas. Good luck :)

本文标签: javascriptMost performant way to transformmove large number of DOM elementsStack Overflow