Core content of this paper:

  1. Core to solve the background system, when there are multiple large amount of data at the same time drop down list resulting in page lag, unable to respond to the problem.
  2. The solution uses virtual list. Based on vue2.5 + element2.0, a layer of encapsulation of el-select is implemented to achieve the virtual list of el-select drop-down.

I believe that the children here, may have encountered the same problem, and looking for a suitable solution. Without further ado, let’s get straight to the point

First, background explanation

1. Brief background:

  • Recently picked up the demand, a back – end buddy tinkered with the front – end project. A component with many filter boxes on a page that toggles criteria to pull different pull-down data (see the component below), a very common mid-stage interaction.
  • The pull-down data can be obtained in a special way by crawling the content in an SSR page. The specific behavior is probably through the re match out a variable name to get a JS object string, probably a total size of more than 30 k of a string, and finally useeval,new FunctionEtc to turn a string into a normal JS object.

2. Troubleshooting:

  • According to a certain operation path, the page is found to be stuck, basically a collapse body, no response to any UI interaction, can only respond to the delay. From the above representation, from:

    1. The JS thread is blocked. Monitoring the JS execution time of performance panel, no obvious js execution slow occupation thread problem was found. Worker was also used, and no improvement was found in woker execution of the code converting strings into JS objects.
    2. Memory screening. The initial method used to convert js strings to JS objects is directeval(str)From the Internet search relatedevalThe problem of memory freeing multiple times, andevalDirect and indirect execution ofThis article on Eval), no improvement after trying.
    3. Dom node problems. When I was working on the project before, I encountered the phenomenon of the whole page being stuck, which was caused by a large number of DOM operations in an instant, resulting in the TAB process of the browser being stuck. After trying to comment out parts of the DOM and leaving the JS execution, the page interaction is incredibly silky. Well, that solves the case

3. Simple analysis:

  • usedocument.getElementsByTagName('*').length Analyze the number of DOM on the current page
  • As shown in the figure below, these selectsretrieve data based on different criteria, default to the dom count of the first item

  • As shown in the figure below, when the component type criteria are switched, the DOM number increases dramatically

  • As you can see, dom numbers are huge, concentrated in drop-down data such as regions and carriers, and browsers render a lot of DOM at once. This is also the cause of subsequent switch conditions, where the entire browser tabbed and became unresponsive (the browser did a lot of DOM adding and deleting).

Second, solutions

1. Use element’s remote search method

  • Remote search is definitely a relatively low-cost solution to these problems. However, it was also introduced in the background that the data of this project was taken down by crawling the SSR page, which could not be directly transformed into remote loading method to obtain the data.
  • The front end implements remote search. Pull back the data stored in memory, throughfilter-methodCustom search, simulating remote search. One problem with this, however, is that a search for a keyword that is repeated often results in too much data being loaded at once and is therefore deprecated.

2. Encapsulate a layer of el-SELECT to implement a virtual list

  • Virtual list is definitely a sharp tool for front-end optimization, which can solve many page performance problems. In this paper, cross-monitor IntersectionObserver + padding is used to implement virtual list.

  • For details about the virtual list, see: Infinite scrolling of the virtual list

  • Let’s talk about component implementation:

    1. Sub-elements

    2. and < Li class=”end” /> are added to el-SELECT as the start and end marks of the display area, and IntersectionObserver is used to monitor them

  • Control whether the element is inserted into the DOM based on the current startIndex and endIndex via v-if control. NowIndex >= startIndex && nowIndex < endIndex

  • By calculating the height of a LI, multiply the startIndex to act as the padding-top of the scroll list, so that the top faded out of the viewable area and there is a space for the destroyed DOM element to ensure the normal scrolling list and realize specific virtual scrolling at one time.

  • Finally, post a sketch

Out of the box

  • Post the entire component code directly to the developers who really need it

  • You can use it exactly as you would with Element’s Select component, except that you don’t have to do the V-for step of el-Option yourself. Once plugged in, you can refer directly to element2.0’s select usage documentation. Pass array: [{label: “, value: “}] If customization is required, you can pass in an Arrange function to implement it yourself

  • <template>
      <el-select
        ref="elSelectRef"
        v-model="proxyValue"
        :filter-method="selectFilter"
        v-bind="$attrs"
        v-on="$listeners"
        @visible-change="handleVisible"
      >
        <li class="start" />
        <template v-for="(item, i) in optionsDuplicate">
          <el-option
            v-if="isRender(i)"
            ref="elOptionItem"
            :key="item.value + i"
            :label="item.label"
            :value="item.value"
          />
        </template>
        <li class="end" />
      </el-select>
    </template>
    
    <script>
      import cloneDeep from 'lodash.clonedeep'
      
      const maxRender = 60
      const refreshRender = 30
      let listItemHeight = 34.// Defaults the height of each list
          fatherUlDomNormalPaddingTop = 6 // Default drop ul paddingTop
    
      export default {
        name: "BaseSelect".props: {
          options: {
            type: Array.default () {
              return[]}},value: {
            type: [String.Array.Number].default () {
              return []
            }
          }
        },
        data () {
          return {
            observer: Object.create(null),
            startIndex: 0.optionsDuplicate: [].fatherUlDom: Object.create(null)}},computed: {
          proxyValue: {
            get () {
              return this.value
            },
            set (val) {
              this.$emit('update:value', val)
            }
          },
          isRender () {
            return i= > i >= this.startIndex && i < this.endIndex
          },
          endIndex () {
            return this.startIndex + maxRender
          }
        },
        watch: {
          options (val) {
            this.optionsDuplicate = cloneDeep(val)
            this.$nextTick(_= > this.handleVisible(true}})),methods: {
          selectFilter (enterStr) {
            this.initData()
            if(! enterStr) {this.optionsDuplicate = this.options
              return
            }
            this.optionsDuplicate = this.options.filter(item= > item.label.includes(enterStr))
          },
          handleVisible (isVisible) {
            if(! isVisible || !this.$refs.elOptionItem) {
              this.observer.disconnect && this.observer.disconnect()
              return
            }
    
            this
              .initData()
              .initObserve()
          },
          initData () {
            this.startIndex = 0
            this.fatherUlDom.style && (this.fatherUlDom.style.paddingTop = fatherUlDomNormalPaddingTop + 'px')
    
            return this
          },
          initObserve () {
            this.$nextTick(() = > {
              const listDomVm = this.$refs.elOptionItem[0]
              if(! listDomVm)return
    
              const listDom = this.$refs.elOptionItem[0].$el
              listItemHeight = listDom.offsetHeight || listItemHeight
              this.fatherUlDom = listDom.parentElement
              fatherUlDomNormalPaddingTop = parseFloat(window.getComputedStyle(this.fatherUlDom).paddingTop) || fatherUlDomNormalPaddingTop
              // Find the dropdown DOM in the elSelect instance
              const dropDownDomVm = this.$refs.elSelectRef.$children.find(_= > _.$el.className.includes('el-select-dropdown'))
              if(! dropDownDomVm)return
              const dropDownDom = dropDownDomVm.$el
              const [startDom, endDom] = [dropDownDom.querySelector('.start'), dropDownDom.querySelector('.end')]
    
              this.observer = new IntersectionObserver((entries) = > {
                console.log('entries', entries)
                if (entries.length >= 2) return // Avoid logical problems when start and end enter the viewable area at the same time when elements are deleted alternately
                const dom = entries[0] // select the first one to judge
                if(! dom.isIntersecting)return
                console.log(dom.intersectionRatio, dom)
                if (dom.target === endDom) {
                  // Scroll down
                  console.log('first'.this.startIndex)
                  const resultIndex = this.startIndex + refreshRender
                  this.startIndex = resultIndex > this.optionsDuplicate.length ? this.startIndex : resultIndex
                  console.log('second'.this.startIndex)
                } else {
                  // Scroll up
                  const resultIndex = this.startIndex - refreshRender
                  this.startIndex = resultIndex < 0 ? 0 : resultIndex
                }
                this.fatherUlDom.style.paddingTop = this.startIndex * listItemHeight + fatherUlDomNormalPaddingTop + 'px' // Fill height
              })
              this.observer.observe(startDom)
              this.observer.observe(endDom)
            })
          }
        }
      }
    </script> 
    Copy the code
  • If you want to retrieve the corresponding DOM element of the element, you can retrieve it directly from the element instance. For example, in the above code, the corresponding el-select drop-down box is not directly retrieved from the DOM API. Instead, $children is retrieved in this component instance

  • Business requirements for this padding that cannot be covered:

    1. Items in the list vary in height. If the component is not well used in this scenario, the height of the item can be restricted from the layout, not folded, and overflow hidden, unless it is mandatory
  • Completed ~ after the trial, the perfect solution to the problem of page lag, and normal business functions are not affected. If you find any bugs you can feedback, I will continue to optimize and follow up ~

  • Finally, if you have any better advice, program, hurriedly tell me, let younger brother learn to learn ~ 😄