Some performance issues



  • Hi, I know, that quasar is not in performance/optimisation/efficiency mode (yet) but maybe it would be interesting for someone. In one of my apps I noticed a large raise of non referenced DOM nodes (removed from DOM but stored in process memory) and attached corresponding javascript event handlers (stored in javascript heap memory). Those nodes and events are not garbage collected - looks like memory leak. Even after pressing F5 they are not gc processed. They are removed only after pressing Ctrl+F5, so it is rather a problem for a typical user.

    After little tinkering, I isolated the most leaking code - it was placed in QTooltip. Maybe someone wiser/more experienced could confirm/reject my observation:

    Here is the code:
    0_1533031981742_e35740a3-0a06-4de8-9420-abcafc8cc493-image.png

    And here are example memory spikes (paging through QDataTable) and DOM Nodes count. This $nexttick handler is present in many groups (recalculate style, layout etc.):

    0_1533032147593_9ce4e10e-0b17-46a9-9bdb-020238cd83d2-image.png



  • I have the same issues with QDataTable. It seems like QDataTable slows down my web browsers after a few cycles of pagination and filter actions.


  • Admin

    1. Quasar IS in in performance/optimisation/efficiency mode. But it depends on the usage. If you render 1 million components then any framework would start to cope hard with this.
    2. This is NOT an issue. The QTooltip (and QPopover, and QModal) are kept in memory and added to DOM only when they are displayed as long as the components are being rendered by Vue. The Garbage Collector works when their parent component gets destroyed (which makes sense). This is the required behaviour because tooltips, popovers and modals need to be dynamically added at the end of <body> so they get displayed on top of any other page content – and not in the same place as their parent components (which would raise another issue of inheriting CSS props from it too).


  • @rstoenescu
    Hi, thanks for reply. Let me clarify:

    1. the problem is observable with only 100 rows per page in QDataTable case. It is a show stopper for SPA apps on older mobile devices.

    2. as you could see on the attached screenshot, both handlers and references were not GC collected when they were no longer displayed. The listeners, nodes and js heap only grows after clicking next page in QDataTable with custom QTooltips. It seems, that the problem may be not with the adding new tooltips, popovers and modals but with disposing those no longer needed. I do not know if rows (ie dom nodes and events) in paginated table are reused but tooltips and their handlers obviously are not.

    OK, I’m not arguing anything specific, just making an observation. I’m sure that it will somehow feed your (profiling/speed/memory consumption) curiosity anyway 🙂


  • Admin

    Ok, now it’s more clear to me what you’re reporting. Will investigate. Might be related to how Vue handles the memory. Will see.


  • Admin

    Without a shadow of a doubt it’s not QTooltip and it’s not Quasar.

    It’s either Vue or something else (like how browser schedules for GC). I also did a quick test on https://vuejs.org/v2/guide/transitions.html#Custom-Transition-Classes --> check the example on this section, and hit “Toggle Render” a few times. DOM node count rises up. This is with very simple code:

    <div id="example-3">
      <button @click="show = !show">
        Toggle render
      </button>
      <transition
        name="custom-classes-transition"
        enter-active-class="animated tada"
        leave-active-class="animated bounceOutRight"
      >
        <p v-if="show">hello</p>
      </transition>
    </div>
    
    new Vue({
      el: '#example-3',
      data: {
        show: true
      }
    })
    

    This should be reported to Vue team.



  • It can be related with this issue:
    https://github.com/vuejs/vue/issues/8507



  • @rstoenescu
    hi,

    I think you’re right that it is not a QToolTip. I created a jsfiddle

    https://jsfiddle.net/unLms60y/59/

    with this html:

    <link rel="stylesheet" type="text/css" href="https://unpkg.com/quasar-framework@^0.17.0/dist/umd/quasar.mat.min.css">
    <script src="https://unpkg.com/quasar-framework@^0.17.0/dist/umd/quasar.mat.umd.min.js"></script>
    
    <div id="q-app">
    <q-btn @click="fetchData">first load</q-btn>
            <q-table
              title=""
              :data="produkty"
              :columns="columns"
              row-key="id"
              :pagination.sync="pagination"
              :rows-per-page-options="rowsPerPage"
              separator="horizontal"
              dense
              @request="requestDataTable"
            >
              <q-tr slot="header" slot-scope="props">
                <q-th v-for="col in props.cols" :key="col.name" :props="props">
                  <strong>{{col.label}}</strong>
                </q-th>
              </q-tr>
              <template slot="body" slot-scope="props">
                <q-tr :props="props">
                  <q-td key="id" :props="props">
                    {{props.row.id}}
                    &nbsp;<q-btn round dense icon="add_shopping_cart" color="primary" small @click="cartAdd(props.row)"><q-tooltip>add to cart</q-tooltip></q-btn>
                  </q-td>
                  <q-td key="title" :props="props">{{ props.row.title }}<br><small class="text-dark">({{ props.row.url }})</small>
                    &nbsp;<q-btn round dense icon="add_shopping_cart" color="primary" small @click="cartAdd(props.row)"><q-tooltip>add to cart</q-tooltip></q-btn>
                  </q-td>
                  <q-td key="url" :props="props">{{ props.row.url }}
                    &nbsp;<q-btn round dense icon="add_shopping_cart" color="primary" small @click="cartAdd(props.row)"><q-tooltip>add to cart</q-tooltip></q-btn>
                  </q-td>
                  <q-td key="albumId" :props="props">
                    {{ props.row.albumId }}
                    &nbsp;<q-btn round dense icon="add_shopping_cart" color="primary" small @click="cartAdd(props.row)"><q-tooltip>add to cart</q-tooltip></q-btn>
                  </q-td>
                  <q-td key="thumbnailUrl" :props="props">{{ props.row.thumbnailUrl }}
                    &nbsp;<q-btn round dense icon="add_shopping_cart" color="primary" small @click="cartAdd(props.row)"><q-tooltip>add to cart</q-tooltip></q-btn>
                  </q-td>
                </q-tr>
              </template>
            </q-table>
    </div>
    

    and this javascript:

    new Vue({
      el: '#q-app',
      data: function () {
        return {
          produkty: [],
          pagination: {page: 1, rowsPerPage: 5, _sortBy: null, descending: false, rowsNumber: 1000},
          rowsPerPage: [0, 5, 10, 15, 25, 50, 100],
          columns: [
            {name: 'albumId', field: 'albumId', label: 'albumId', required: false, align: 'left', sortable: false},
            {name: 'id', field: 'id', label: 'id', required: false, align: 'left', sortable: false},
            {name: 'title', field: 'title', label: 'title', required: true, align: 'left', sortable: false},
            {name: 'url', field: 'url', label: 'url', required: true, align: 'left', sortable: false},
            {name: 'thumbnailUrl', field: 'thumbnailUrl', label: 'thumbnailUrl', required: true, align: 'right', sortable: false}
          ]
        }
      },
      mounted () {
        // this.fetchData()
      },
      methods: {
        async requestDataTable ({pagination}) {
          this.pagination.page = pagination.page
          this.pagination.rowsPerPage = pagination.rowsPerPage
          await this.fetchData()
        },
        async fetchData () {
          this.produkty = []
          // (this.pagination.page - 1) * this.pagination.rowsPerPage, this.pagination.rowsPerPage
    			response = await fetch('https://jsonplaceholder.typicode.com/photos/')
          json = await response.json()
          this.produkty = json.slice(0,this.pagination.rowsPerPage)
        },
        cartAdd (item) {
          this.$q.notify('add: ' + item.title)    
        }
    	}
    })
    

    the profiling session:
    click first load button
    click next page
    change records per page to 100
    click next page
    click some qtooltips
    click next page
    click next page
    change records per page to 5
    click next page
    click next page

    …and it seems OK:

    0_1534284531793_0fab5113-b32a-4ae6-8575-6bfd778b8896-image.png

    I was curious if changing records per page from 100 to 5 will release memory and it released as you said - which you can see on the chart.

    So, in my application the memory leak still exists. It needs further investigation. As for now I’m assuming that it is some combination of vue, vuex, router, i18n, quasar interaction (ha ha).

    BTW debugging such leaks is extremely hard. I really would like to have some “developer mode” where quasar would scream to me (in console log) - “hey - you’re leaking those objects and it’s not my fault, you should check this and that”. Oh, sweet dreams 🙂


  • Admin

    Like I showed you earlier, it should be reported to Vue with the simple example (that I copy-pasted) from their docs.


  • Admin

    Just open that link in vue docs and see the DOM node count rising up.