preface

I’ve researched a lot of performance documents and blogs, and found that 90% of the performance articles are from previous ones, but can performance optimization be as good as it is today?

Obviously, no, technology is a gradual process, but now it’s more of a rolling over of the current content, SO I’m ready to find a new way out

The birth of an idea

In fact, we now have a lot of performance optimization detection and performance optimization solutions, from the perspective of development to user experience, there are different detection and processing solutions, the most popular in the market is the following:

  • Development phase (public variables, common styles, component extraction, data processing algorithms, use workers (except elements) that affect page rendering speed and user response, etc.)
  • Pack build (GZIP compress, go log, go Sourcemap, import on demand, load on demand, image style merge, reduce pack time and pack size, add cache, etc.)
  • Release Phase (CI, CD)
  • Resource optimization (strong caching, negotiated caching, resource preloading, asynchronous loading, service-worker, etc.)

Of course, there’s more to it than that, but I’m just making a list of the things that I usually use, For example, I have previously written a practical article – how to achieve the same modular loading (task-silce) as taobao mobile terminal and analytical article – Task-slice to achieve taobao mobile terminal mode loading, which is the performance optimization in the development phase of comparing the details of user experience. Of course, we can also do the detection before performance optimization based on the Performance API, which is exactly what I have sorted out before.

After thinking about these things, I still feel that the performance optimization is not detailed enough, which has many disadvantages:

  • Pseudo performance optimization (this means that the performance optimization is not done thoroughly)
  • – Unable to fully grasp the data related to the DOM rendering of the page (the flame diagram is too complex to see, there is no data)
  • throughperformance.markThe way of implantation may be a great cost for the project. A lot of invalid code will be implanted in the business to test the user experience, and it may affect the business in some cases, or cause some conditions of the businessperformance.markCan’t accurately grasp, so overall can’t really achieve the goal of perfection

At this time, I thought about how to avoid these problems and accurately capture the rendering time of the current element. Baidu and Google found an API that seemed to solve this problem after a period of time, so I started to start

Realization of ideas

To do this, we need to sort out our needs:

  • Captures the rendering time of the current element (when to start, how long to render, and where to render)
  • This requirement is achieved without embedding performance detection-related code into the business
  • Where the captured information is previewed (should we build a performance testing platform for this optimization when the company does not have a performance testing platform)
  • Is it possible to display the data through a browser plug-in (so that it is easy to preview without affecting business in all directions)

Once you have the idea, all that’s left is to implement it

Captures the render time of the current element

In fact, the main functionality described in this article is implemented based on this API, which is the element elementTiming attribute

It is also simple to add this attribute to the element currently being tested:

   <div elementtiming="text">Test the text</div>
Copy the code

Then get the corresponding data from the PerformanceObserver object:

    const observer = new PerformanceObserver((list) = > {
      console.log(list.getEntries())
    });
    observer.observe({ entryTypes: ["element"]});Copy the code

Log (elementTiming) : loadTime (loadTime), renderTime (renderTime), etc

Of course, this API only contains other elements (no text) in this element, so it will not generate PerformanceEntry. This problem is that I cannot find it on Baidu online, but I found that the effect is not accurate after looking at the case of MDN. After mentioning the issue to Chromium, the official reply will be given

Vue or React local Server, new PerformanceObserver().obserbe({entryTypes: [‘element’] }) Incomplete acquisition, but build after the project unstable

This process is very complicated, after learning the official reply, I think this API is not perfect, I would like to continue the discussion in the above link comment section, but can not resist the foreign hand to directly turn off the bug

Ok, so I have to start over with a new requirement and discuss it with them:

PerformanceObserver API result not what I expected

After I mentioned this requirement, I was still waiting to discuss my requirement, but he told me that I was not responsible for this here and asked me to go to WICG to ask for the requirement.

Then I went over:

I want a complete tree data table, so I can know the rendering time of each layer of data and the rendering time of the corresponding child level, but it is not good to get the target IMG or text element directly, and save performance.

This is obviously not enough to meet my needs, so I have to explain it to him in detail:

I don’t know whether I have explained clearly, or whether my requirements are also what everyone needs. Welcome to discuss. I will leave my contact information at the bottom or discuss it in this issue

Issue link: PerformanceObserver API return result not what I need

Ok, so much for the presentation of the problems that arise during the research and use of the API and my solutions. The focus is on the overall functionality, which is enough for you to use

This requirement is achieved without embedding performance detection-related code into the business

For example, I didn’t want to embed this in my project, because if it was a really big project, I’d be crying if I wrote a bunch of performance. Marks, which was obviously not practical, and THEN I wondered if I could implement this requirement through WebPack.

It must be possible to parse the current content, and then add the attribute by getting the corresponding resource, but it is not recommended to match the content directly. For example, the content looks like this:

    <div class="a">
        this is <div class="a"> element
    </div>
Copy the code

If you add loader directly to the webpack configuration, then the current loader accesses all the contents of the current package for the entire project. Of course not. Regular expressions kill you

How about using Babel to parse the AST to render, so that you can accurately get the corresponding attribute ah, so that is ok? I’m going in the right direction, but when used directly, Babel will translate all the current content resources, which is obviously not what I need:

// unitl.js
export const fn1 = function() {
    return 1
}

// component.js

export default function() {
    return <div>this is <div class="a"> element </div>
}
Copy the code

If you just use Babel, all of the above files will be translated through Babel, so it is not reasonable for us to do this, and the page will not be built slowly because of the performance of the elements? And it’s not optimal

At this time, I came up with a method, which is also the method I am using now, and you can see whether it is really the optimal solution. So far, I have considered this:

Add a Loader to the current module before the build using the Webpack plugin. Add elementTiming to the current Loader using Babel translation

Know how to do to start the code, the following is the call method:

// webpack.config.js

const ElementRenderingWebpackPlugin = require('element-rendering-webpack-plugin')
module.exports = {
    plugin: [
        new ElementRenderingWebpackPlugin()
    ]
}
Copy the code

Plugin implementation is also relatively simple, the main work is in the Loader part:

// element-rendering-webpack-plugin.js

class MyPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('MyPlugin'.(compilation) = > {
      compilation.hooks.buildModule.tap('SourceMapDevToolModuleOptionsPlugin'.module= > {
        if (module.resource) {
          if (/(\.((j|t)sx?) $) /.test(module.resource) && 
          !/node_modules/.test(module.resource)) {
            if (module.loaders) {
              module.loaders.push({
                loader: 'element-rendering-webpack-loader'})}}}})})}}module.exports = MyPlugin
Copy the code

The above code is incompilationGenerated after the modulebuildGo to do module confirmation, only for my own business and need to add the codeloader, so you can bypass the above and use it directlybabelMethod causes build speed problems

Because it is published in 1.0, there are still some things that have not been fully considered and need to be optimized. Here is a reminder that vUE is not supported for the time being. There are too many Loaders for vUE module, so I need to do more tests before I can go online

// element-rendering-webpack-loader.js


const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAstSync } = require('@babel/core');
const t = require('@babel/types');
let randomSet = new Set(a);function UpdateAssets(asset) {
  let code = asset
  try {
    const ast = parser.parse(asset, {
      sourceType: 'module'.plugins: [
        'flow'.'jsx']}); traverse(ast, {JSXElement(nodePath) {
        if (nodePath.node.type === 'JSXElement' && nodePath.node.openingElement.name.name === 'img') {
          return
        }
        updateAttr(nodePath.node);
      }
    })
    code = transformFromAstSync(ast).code;
  } catch(e) {
    console.log(e)
  }
  return code;
}

function updateAttr(node) {
  if (node.type === 'JSXElement') {
    let { openingElement, children } = node;
    let name = openingElement.name.name || openingElement.type
    let className = openingElement.attributes.filter(attr= > {
      if (attr.type === 'JSXSpreadAttribute') return false
      return /class(Name)? /.test(attr.name.name)
    })
    if (className.length) {
      name = className[0].value.value
    }
    if(! openingElement)return
    const elementtimingList = openingElement.attributes.filter(attr= > {
      if(attr.type ! = ='JSXSpreadAttribute' && attr.name.name === 'elementtiming') {
        return true}})if(! elementtimingList.length) { openingElement.attributes.push(addElementttiming(name +The '-' + Math.ceil(Math.random() * 100000)));
    }
    const markList = openingElement.attributes.filter(attr= > {
      if(attr.type ! = ='JSXSpreadAttribute' && attr.name.name === 'data-mark') {
        return true}})if(! markList.length) { openingElement.attributes.push(addMark()); } children.map(childNode= >updateAttr(childNode)); }}function addElementttiming(name) {
  return t.jsxAttribute(t.jsxIdentifier('elementtiming'), t.stringLiteral(name));
}

function addMark() {
  let randomStatus = true;
  let markRandom = 0;
  while(randomStatus) {
    markRandom = Math.ceil(Math.random() * 100000);
    randomStatus = randomSet.has(markRandom);
    if (!randomStatus) {
      randomSet.add(markRandom);
    }
  }
  return t.jsxAttribute(t.jsxIdentifier('data-mark'), t.stringLiteral(markRandom + ' '));
}

module.exports = UpdateAssets;
Copy the code

Here directly on the code, too many things do not explain line by line, the code will be open source, the link at the bottom of their own slowly see

Basically what you’re doing is passing the code that’s currently running inastTranslate, get itastAdd after objectelementtimingProperties,data-markIt’s used to de-weight data

Well, at this point in time, the basics of capturing data and not embedding performance detection-related code into the business are met, so it’s time to present the data through a browser plug-in

This data is presented through a browser plug-in

Version 3 does not support many of the things that version 2 can use

Here I directly on the core part of the code, the rest of the basic configuration class you will see the code:

// contentScript.js

chrome.runtime.onMessage.addListener(function(request) {
  const { type, data } = request.data
  switch(type) {
    case 'selectedElement':
      createMask(data)
      break;
    case 'cancelElement':
      cancelMask()
      break; }})function createMask(data) {
  cancelMask()
  const div = document.createElement('div')
  Object.keys(data).map(styleKey= > div.style[styleKey] = data[styleKey] + 'px')
  div.style.position = 'absolute'
  div.style.background = 'rgba (109, 187, 220, 0.5)'
  div.style.zIndex = '9999'
  div.id = 'mask-element'
  document.body.appendChild(div)
}

function cancelMask() {
  const maskElement = document.querySelector('#mask-element')
  if(maskElement ! = =null) {
    document.body.removeChild(maskElement)
  }
}

function getElementTreeData(element, elementTreeData, performanceElementTimingObj) {
  let children = element.children
  for (let i = 0; i < children.length; ++i) {
    let childElement = children[i]
    let argObj = {}
    let nodeValue = ' '
    let parsePerformanceElementTiming = {}
    if ('elementtiming' in childElement.attributes) {
      nodeValue = childElement.attributes.elementtiming.nodeValue
      argObj['elementtiming'] = true
      argObj['key'] = childElement.dataset.mark
      let performanceElementTiming = performanceElementTimingObj[argObj['key']]
      if (performanceElementTiming) {
        parsePerformanceElementTiming = JSON.parse(JSON.stringify(performanceElementTiming))
      }
    } else {
      nodeValue = childElement.nodeName
      argObj['key'] = Math.ceil(Math.random() * 100000)
    }
    argObj = Object.assign({}, argObj, parsePerformanceElementTiming, {
      intersectionRect: childElement.getBoundingClientRect()
    })
    if (/(NO)? SCRIPT/.test(nodeValue)) continue
    argObj['children'] = childElement.children.length ? getElementTreeData(childElement, [], performanceElementTimingObj) : []
    argObj['title'] = nodeValue.replace(/ - ([0-9] *) $/.' ')
    elementTreeData.push(argObj)
  }
  return elementTreeData
}

let performanceElementTimingList = []
const observer = new PerformanceObserver((list) = > {
  let elementTree = []
  let performanceElementTimingObj = {}
  performanceElementTimingList = performanceElementTimingList.concat(list.getEntries())
  performanceElementTimingList.map(performanceTimingItem= > {
    if(performanceTimingItem.element ! = =null) {
      return performanceElementTimingObj[performanceTimingItem.element.dataset.mark] = performanceTimingItem
    }
  })
  chrome.runtime.sendMessage(
    {
      type: 'performanceTree'.data: getElementTreeData(document.body, elementTree, performanceElementTimingObj)
    }
  )
});
observer.observe({ entryTypes: ["element"]});Copy the code

ContentScript is a configuration file for accessing page elements in Chrome-Extension. Of course, I use the name of the file at will. To make it easier to read and understand, I just follow the rhythm of the official document. CreateMark is used to create elements and position them. It is used in conjunction with the devTools tree.


// app.js 
import { useState, useEffect } from 'react';
import { Tree } from 'antd';
import './App.css';
function App() {
  const [treeData, setTreeData] = useState([]) 
  window.addEventListener('message'.msg= > {
    const { type, data } = msg.data
    if (type === 'performanceTree') {
      setTreeData(data)
    }
  })
  useEffect(() = > {
  }, [treeData])
  return (
    <div className="App">
      <Tree
        showLine
        titleRender={
          nodeData= > {
            return (
              <div onMouseOver={()= > { selectedElement(nodeData) }} onMouseOut={cancelElement}>
                {nodeData.title}{updateTime(nodeData)}
              </div>
            )
          }
        }
        treeData={treeData}
      />
    </div>
  );
}

function updateTime(nodeData) {
  let str = The '-'
  if (nodeData.renderTime) {
    str += Math.round(nodeData.renderTime)
  } else {
    str += 'There is no text under or outside the element.'
  }
  return str
}

function selectedElement(nodeData) {
  console.log('selectedElement')
  if(! nodeData.disabled) { postMessage( {type: 'selectedElement'.data: nodeData.intersectionRect
      },
      The '*')}}function cancelElement () {
  console.log('cancelElement')
  postMessage(
    {
      type: 'cancelElement'
    },
    The '*')}export default App;
Copy the code

For the beauty of the page, I used ANTD to optimize the UI of the page. When clicking on a tree, a box will be drawn to indicate the time of the current element and the location of the corresponding element:

This is the final result, I installed it directly after the React scaffolding was built

The end of the

The implementation of the general idea and thinking process, basically I have described almost, there are many times in the process of giving up, but do not have the heart to abandon their previous pay so adhere to it, also can be done, but elementTiming API there that problem, or I need to continue to study and solve, I will continue to communicate with WICG to make it better

Maybe some big guy will see this thing is very simple, there is nothing worth thinking about, then I just want to say DDDD, I am relatively vegetables, have to learn step by step, you gently spray ha

Code has been open source, welcome to discuss with each other to learn, but also hope you give a little star, more issue, if interested in friends I also hope that we together to maintain this thing:

  • plugin: element-rendering-webpack-plugin
  • loader: element-rendering-webpack-loader
  • extension: element-rendering-extension

Public account: global Internet technology sharing, attention will be sent to the group QR code