Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zTree插件的一个性能问题 #1

Open
susucain opened this issue May 5, 2020 · 0 comments
Open

zTree插件的一个性能问题 #1

susucain opened this issue May 5, 2020 · 0 comments

Comments

@susucain
Copy link
Owner

susucain commented May 5, 2020

背景

之前做过的项目在通讯录成员有1000个时,网页加载要30s才能加载完全。

想到可能的原因有两种

  1. 接口响应时间过长
  2. js脚本执行时间过长,由于浏览器的渲染是单线程的,js脚本的执行阻塞了UI线程的渲染

定位问题原因

打开Network面板,刷新页面重新加载后发现接口的响应时间只有1.43秒,所以排除是接口响应时间过长导致的问题。

image

排除掉接口响应时间长的问题后,用Performance面板分析是否是脚本执行时间过长导致的问题。

点击Record按钮后重新加载通讯录,待通讯录加载完毕后结束录制,查看。

其中黄色部分是脚本执行的时间,发现脚本执行时间占了大部分。

image

至此,确定是脚本执行时间过长阻塞了浏览器渲染。

查看在此期间的函数调用栈,发现createNodeCallback函数执行时间长达29.75秒,而这个函数调用了数次apply方法和addDiyDom方法。

找到函数的定义位置,发现函数执行了1000次的循环,循环调用addDiyDom方法。如果setting.callback.onNodeCreated存在还会触发1000次NODECREATED事件。而这两项都是挂在setting,也就是用户的配置上。

image

由于项目的配置中没有onNodeCreated项,所以在代码中注释掉addDiyDom方法,再次用Performance面板分析

image

可以看到脚本的执行时长大大减少。

查看addDiyDom方法

image

该方法查找了页面上的DOM元素,并直接对页面上的DOM元素进行了修改;而在JS中对DOM的操作最是损耗性能。

至此,问题原因定位完毕,由于zTree插件连续调用1000次对页面上的dom节点直接进行修改的addDiyDom方法,造成了脚本执行时间过长,页面卡顿的性能问题。

寻找解决方式

查看源码发现createNodeCallback方法是在appendParentULDom方法之后调用,而appendParentULDom方法所做的正是把zTree插件生成的dom树插入到页面中。所以,用户定义的addDiyDom方法就只能对页面上的DOM直接进行操作了。

image

由于业务需求,需要在人员节点上增加两个按钮。可不可以在生成节点的时候就吧自定义按钮插入进去呢?这样就可以避免直接操作页面上DOM的性能问题

在官网的API中也没有找到可以在生成节点的时候把用户自定义的节点插入的方法。只好再翻一下源码。

找到生成dom节点的位置

image

在这些方法中一个个找可以插入节点的地方,zTree的节点结构是固定的,外层是一个li元素,第一个子节点span元素用作(展开/收起)的按钮,接下来是一个a标签。a标签中第一个元素是节点名称前的图标,第二个是展示用户配置数据的节点名称data[i].name

image

源码中第一个方法生成li元素的开始标签部分;第二个方法生成span.switch元素;第三个方法中查询用户配置,如果用户配置了setting.check.enable为true,则会在a元素前插入一个选择框;第四个方法生成a标签的开始部分;第五个方法从名称上看是向a元素前插入节点,前一个方法刚好生成了a标签的开始部分,如果这个方法可以插入节点,问题就能解决。似乎看到了一丝希望。

断点打到getInnerBeforeA内部

image

啊嘞嘞,这个函数要想起作用,必须_init.innerBeforeA数组中有方法才行,那有什么办法可以向_init.innerBeforeA数组push函数?

在文件中查找innerBeforeA

image

看来要想往_init.innerBeforeA数组push函数得调用data.addInnerBeforeA才行,那data这个对象有没有暴露给用户呢?

通过VS Code查找这个对象的引用

image

果然是有暴露给用户的~

这样就可以在zTree初始化前自定义一个方法,将我需要定制的节点插入a元素中。

在该函数中打断点查看调用栈,找到传入的三个参数settingnodehtmlsetting是用户的配置。用户传入的对应数据上的属性会挂在node对象上。html是前面几个方法生成的html数组,正好到a标签的开始部分(最后zTree会调用.join方法转成字符串,再调用jquery的append方法,插入到页面中)。这时只要向a标签中push进我需要定制的节点就可以了。

$.fn.zTree._z.data.addInnerBeforeA(function (setting, node, html) {
    if (setting.view.enableDiyDom && node.dataType === 'orgMember') {
        html.push('<button class="mem-btn-video" btntype="yy" title="云眼"></button>');
    }
});

由于该方法可以拿到用户配置,所以可以自定义配置。在这里自定义配置项:setting.view.enableDiyDom。只有当它为true,且节点类型是成员(orgMember)才会添加自定义按钮。如果页面中多个地方用到了zTree插件,那么必须用自定义配置项去做限制,否则自定义的addInnerBeforeA方法会污染所有用到zTree插件的地方。

添加之后查看元素面板,自定义的元素在其中,且是a元素的第一个子元素。

image

打开Performance面板进行分析

image

可以看到脚本的执行的时长仍在1秒左右,至此,问题解决

总结

  • zTree插件提供的addDiyDom方法在大数据量的加载时会导致显著的性能问题,因为该方法添加自定义节点时只能直接对页面上的DOM直接进行操作。

  • 如果用户配置了onNodeCreated方法,且在该方法中进行了DOM操作,在大数据量的加载中也会导致显著的性能问题,原因与addDiyDom方法相同。

  • 添加自定义节点可以使用zTree的内部方法

    • addInnerBeforeA插入的节点作为a元素的第一个子元素
    • addInnerAfterA插入的节点作为a元素的最后一个子元素
    • addAfterA插入的节点作为a元素后的兄弟元素
  • 如果是在鼠标悬浮时才显示用户的自定义元素,可使用setting.view.addHoverDomsetting.view.removeHoverDom配置项

  • 加载1000个节点,js脚本的执行时间已经达到了1秒,如果要求加载的数据更多,或有进一步的优化要求就只能考虑分页加载了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant