(Hornet’s Nest Technology original content, public ID: MFWtech)
A study from Akamai, based on interviews with 1,048 online shoppers, found that:
-
About 47% of users expect their pages to load in less than two seconds.
-
If the page load time is longer than 3s, about 40% of users will choose to leave or close the page.
For a long time, front-end developers have done a lot of work to improve the user experience during page loading, whether it is in Web or iOS or Android applications. In addition to solving the problem of making web pages display faster, it is also important to improve users’ perception of load wait times. The “chrysanthemum diagram” and the various loading animations derived from it are a common solution, and the following icon will be familiar to both developers and users:
The skeleton screen described in this article is seen as an upgraded version of the Chrysanthemum chart. Inspired by the existing skeleton screen scheme, the front end research and development team of Horcellular e-commerce has realized a method of automatically generating skeleton screen, and has achieved good results in the application of multiple pages in horcellular mall.
What is skeleton screen
The skeleton screen can be understood as showing the user a general structure of the current page composed of gray blocks before the page data is returned or the page is not completely rendered, so that the user can feel that the page is gradually rendered, so that the loading process becomes visually smooth. The generated skeleton screen page is shown as the figure below:
The main advantages of skeleton screen are:
- Users avoid long white pages
- The general structure of the page can be known, reducing the probability that users will leave the page believing that something is wrong
- The visual flow is smoother than the chrysanthemum chart
Two, common front-end skeleton screen scheme
Before choosing a skeleton screen, we considered some other options, such as the possibility of using server-side rendering (SSR) to avoid the front end white screen time. However, it is found that there are too many projects involved, and the construction and deployment of services are also involved. Or a simple pre-rendering via prerender-spa-plugin, which is SPA-friendly but requires additional Webpack configuration, is abandoned due to package sourcing issues, takes too long to download, sometimes fails for no reason, and so on.
After a series of research, we have combed several common skeleton screen solutions in the industry, as well as their advantages and disadvantages.
1. UI skeleton screen
That is, a diagram that matches the style of the first page is provided through the UI to act as a skeleton screen, and the base64 image of the skeleton screen is inserted into the root root node and embedded into the project when webPack is packaged.
This is a crude approach that is relatively easy to implement. However, the disadvantages are also obvious, that is, UI designer support and development involvement, not automatic generation.
2. Handwritten skeleton screen
That is, through handwritten HTML, CSS for the target page to customize the skeleton screen. This way you can copy the actual style of the page. However, once the page style changes due to various reasons, it is necessary to change the style and layout of the skeleton screen again, which greatly increases the maintenance cost.
3. Automatically generate a static skeleton screen
Currently, the open source plug-in page-skeleton-webpack-plugin of Ele. me is attracting more attention. Its specific implementation principle is as follows:
- Generating skeleton screen
Puppeteer controls Handless Chrome to open the skeleton screen page to be generated. After the page is loaded, add and delete elements in the page and overlay existing elements in a layered style to display them as gray blocks on the premise of retaining the page layout style. Then the modified HTML and CSS are extracted, and the page is divided into different block areas, such as text block, image block, button block, SVG, pseudo-class element block, etc., respectively, each block is processed to make it as consistent as possible with the original page. The addScriptTag method of the Puppeteer Page instance is used to insert the script that handles the block into the page opened by Headless Chrome.
There may still be a gap between the actual generated skeleton screen page and the original page. The plug-in writes the skeleton screen into the memory through memory-FS, and can edit and preview the generated skeleton screen twice through the preview page. After modification, click the generate button to generate a new skeleton screen and write it into the project.
Borrow a picture to illustrate:
- Insert frame panel
The SKELETON screen DOM structure and CSS are generated offline, injected at build time into the template (EJS) below the node, and inserted into the HTML in the after-emit hook function.
Page -skeleton-webpack-plguin Can generate skeleton screen pages according to different routing pages in a project, and pack skeleton screen pages into corresponding static routing pages through Webpack.
Its disadvantages are:
-
In actual use, the interface return cannot be monitored, resulting in the accuracy of the generated skeleton screen
-
The generated pages are directly related to the quality of the structure written by the business people and often require manual retuning
In this context, the r&d front end team of Horcellular e-commerce hopes to find a skeleton screen generation method that is more friendly to development while improving user experience. It can automatically generate similar skeleton screens for different business scenarios and realize automatic injection. For development, a skeleton screen can be generated with a single command or simple configuration, with no need to worry about subsequent maintenance.
In the process of scheme investigation, draw-Page-structure provided inspiration for our design.
4. draw-page-structure
- Generate skeleton screen:
// dps.config.js
{
url: 'https://baidu.com',
output: {
filepath: '/Users/famanoder/DrawPageStructure/example/index.html',
injectSelector: '#app'
},
background: '#eee',
animation: 'opacity 1s linear infinite; ',
// ...
}
Copy the code
According to the online address specified by THE URL, Puppeteer can obtain the DOM structure of the current page and generate the skeleton screen file of the element nodes into the file specified by Filepath to generate the skeleton screen page, as shown below:
-
Insert frame panel
Insert the skeleton screen file generated above into the node id=”app” under the root node of the page, and then provide the method of active skeleton screen destruction in the general tool, you can help develop active control or destruction of skeleton screen, display the real content of the page.
The design idea of draw-Page-structure can meet our needs to a large extent, but the deficiency is that it can only generate skeleton screen for the EXISTING URL on the line, which does not support the development environment. In addition, because it is automatically generated, the generated skeleton screen may not be as expected if the page is redirected to the login page if not logged in. Moreover, its internal implementation is not perfect, which may lead to the need for secondary optimization of the skeleton screen generated under some complex pages.
So we started to explore further.
Third, more development-friendly implementation solutions
1. Design idea
Based on the existing scheme, we came up with the method of specifying the URL of the page to generate skeleton screen and the directory of the file output in the configuration file, reading the configuration items in the configuration file at run time, opening the specified page through Puppeteer and infusing evaldom.js. Because this JS is executed inside the Puppeteer, the complete DOM structure of the current page is available, which leaves a lot of room to play with.
We start with the body tag in the DOM structure, recursively process all the nodes on the page, and then replace the original element with the generated DIV. In the first version of the scheme, getBoundingClientRect and getComputedStyle methods are used to obtain all the calculated attributes of the element and the width, height and position relative to the viewport. Then, the recursive rendering is combined with the style attributes of the element itself to preserve the original DOM nesting level of the page.
However, there are too many attributes that can determine the position of elements, such as position, Z-index, width, height, top, display, box-sizing, flex, etc., which need to be considered, leading to the inability to focus on the logic of processing the DOM structure of the page. These attributes also need to be added to the style of the resulting skeleton screen node after processing, so that the skeleton screen file may be larger than the original full page structure, which is definitely not desirable.
The optimized scheme uses getBoundingClientRect and getComputedStyle to obtain the relevant attributes of elements, and then directly generates the final skeleton screen node through absolute positioning. The final required attributes on the page are position, Z-index, top, left, width, height, background, and border-radius. Except that the original DOM structure of the page cannot be guaranteed, other requirements can be basically met, and the processing of nodes is more focused.
The main implementation process is as follows:
At present, this scheme is mainly applied to the multi-page project of hornet cell e-commerce business, including the single page, visa page, etc. The following single page is taken as an example, and the display effect is shown as follows:
2. Implementation method
- Generating skeleton screen
(1) the config. Js configuration
Const dpsConfig = {// The default location is the current project's skeleton folder, the existing skeleton screen will not be generated again, new page configuration only need to add new entries visa_guide: {url:'https://w.mafengwo.cn/sfe-app/visa_guide.html?mdd_id=10083', // Mandatory}, call_charge: {url:'http://localhost:8081/sfe-app/call_charge.html? rights_id=25', // Mandatory item to generate skeleton screen page address, using Baidu (https://baidu.com) can also try // URL:'https://www.baidu.com',
device: 'pc', // Optional, default mobile background:'#eee', // Optional animation:'opacity 1s linear infinite; ', // Optional headless:false, // Optional customizeElement:function(node) {// Optional // Return value enumeration if yestrueSays it will not recursively until this layer down, if the return value is an object so the value of the node is according to the style of the object inside draw / / if the return value of 0 indicates normal recursive rendering render / / if the return value of 1 indicates the current node is not recursive down / / if the return value is 2 said do not make any processing to the current nodeif(node.className === 'navs-bottom-bar') {return 2;
}
return 0;
},
showInitiativeBtn: true// Optional if this value is set totrueIt indicates that the development needs to actively trigger the generation of skeleton screen, in this case, the headless should be set tofalse
writePageStructure: functionFs.writefilesync (filepath, HTML); (HTML) {fs.writefilesync (filepath, HTML); // console.log(html) }, init:functionModule.exports = dpsConfig; () {// Optional // operations before the skeleton screen was generated, such as removing interfering nodes}}} module.exports = dpsConfig;Copy the code
(2) Puppeteer opens a new page and returns to the browser instance and openPage
const ppteer = require('puppeteer');
const { log, getAgrType } = require('./utils');
const insertBtn = require('.. /insertBtn');
const devices = {
mobile: [375, 667, 'the Mozilla / 5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'],
ipad: [1024, 1366, 'the Mozilla / 5.0 (the device; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1'],
pc: [1200, 1000, 'the Mozilla / 5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1']}; asyncfunction pp({device = 'mobile', headless = true, showInitiativeBtn = false}) { const browser = await ppteer.launch({headless}); // Return browser instance asyncfunction openPage(url, extraHTTPHeaders) {
const page = await browser.newPage();
let timeHandle = null;
if(showInitiativeBtn){
browser.on('targetchanged', async ()=>{// listen for page routing changes and get the latest page of the current TAB page in showInitiativeBtn astrueConst targets = await browser.targets(); const currentTarget = targets[targets.length - 1] const currentPage = await currentTarget.page(); clearTimeout(timeHandle)setTimeout(()=>{
if(currentPage){
currentPage.evaluate(insertBtn);
}
},300)
})
}
try{
let deviceSet = devices[device];
page.setUserAgent(deviceSet[2]);
page.setViewport({width: deviceSet[0], height: deviceSet[1]});
if(extraHTTPHeaders && getAgrType(extraHTTPHeaders) === 'object') {
await page.setExtraHTTPHeaders(new Map(Object.entries(extraHTTPHeaders)));
}
await page.goto(url, {
waitUntil: 'networkidle0'// Trigger when there is no more network connection (after at least 500ms)}); }catch(e){ console.log('\n');
log.error(e.message);
}
return page;
}
return {
browser,
openPage
}
};
module.exports = pp;
Copy the code
(3) Execute the main logic of evaldom.js and evaldom.js to handle node nodes in the browser environment
agrs.unshift(evalScripts); //evalScripts = require('.. /evalDOM'); Executed in puppeteerevalDom.js and passes the parameters configured in config.js toevalDom
html = await page.evaluate.apply(page, agrs);
Copy the code
//evalDom.js main logic startDraw:function () {
const $this = this;
const nodes = this.rootNode.childNodes;
this.beforeRenderDomStyle();
function childNodesStyleConcat(childNodes) {
for (leti = 0; i < childNodes.length; i++) { const currentChildNode = childNodes[i]; // Current child node // Which nodes need to skip drawing the skeleton screenif ($this. ShouldIgnoreCurrentElement (currentChildNode)) {/ / should ignore the current node, do not take any measures. This is where the user can then specify which nodes should be omitted, todocontinue; } const backgroundHasurl = analyseIfHadBackground(currentChildNode); const hasDirectTextChild = childrenNodesHasText(currentChildNode); // Determine if the current element has a direct child element and that element is Textif ($this.customizeElement && $this.customizeElement(currentChildNode) ! = = 0 &&$this.customizeElement(currentChildNode) ! == undefined) {// Developers need to customize the node to render appearance, default returnfalseRepresents processing using a normal recursive algorithm. If the return value istrueIt's not going to recurse down, but if the return value is an object then it means that you need to customize the style and you draw it. todoif (getArgtype($this.customizeElement(currentChildNode)) === 'object') {
console.log('object'); // If an object is returned, it means that the object wants to define the last object to draw}else if ($this.CustomizeElement (currentChildNode) === 1) {// If this time returnstrue, to filter getRenderStyle(currentChildNode); }else if ($this.customizeElement(currentChildNode) === 2){
continue ;
}
continue;
}
if(backgroundHasurl || analyseIsEmptyElement(currentChildNode) || hasDirectTextChild || ShouldDrawCurrentNode (currentChildNode)) {// If the current element is inline, or if the current element is not inline, but contains no children, or if all children are inline then we draw this node on the current skeleton screen. getRenderStyle(currentChildNode, hasDirectTextChild); }else if(currentChildNode.childnodes && currentChildNode..childnodes. Length) {/ / if the current node has child nodes / / recursion childNodesStyleConcat(currentChildNode.childNodes); } } } childNodesStyleConcat(nodes);return this.showBlocks();
},
Copy the code
-
The above rootNode is the rootNode, which defaults to document.body or can be specified by development
-
The main logic is to determine whether the current node needs to be ignored, whether the background picture is set, whether the text information is contained, whether the development has specified the processing mode of the current node, etc. Render the skeleton screen node corresponding to the condition, otherwise, process the child nodes of the current node
-
After all nodes are processed, call showBlocks to splice the generated skeleton screen nodes with bits of HTML string for subsequent processing
(4) getRenderStyle generates skeleton screen style
const styles = [
'position: fixed',
`z-index: ${zIndex}`,
`top: ${top}%`,
`left: ${left}%`,
`width: ${width}%`,
`height: ${height}% `,'background: '+(background || '#eee')]; const radius = getStyle(node,'border-radius'); radius && radius ! ='0px' && styles.push(`border-radius: ${radius}`);
blocks.push(`<div style="${styles.join('; ')}"></div>`);
Copy the code
- ZIndex, top, left, width, height are the processed attributes, and then push all the skeleton screen strings into blocks.
(5) The HTML file of skeleton screen is as follows:
<html><head></head>
<body><div style="position: fixed; z-index: 999; Top: 89.805%; Left: 4.267%; Width: 91.467%; Height: 11.994%; background: #eee"></div></body></html>
Copy the code
- Insert frame panel
Add to the project entry index.html file
<body>
<div id="app">
</div>
<% if(htmlWebpackPlugin.options.hasSkeleton) { %>
<div id="skeleton"> <! Skeleton - screen through htmlWebpackPlugin when start packing automatic injection - > < % = htmlWebpackPlugin. The options. Loading. HTML % > < / div > < %} % > <! -- built files will be auto injected --> </body>Copy the code
Four,
At present, the scheme already supports the developer to actively control the skeleton screen generation time, which avoids the failure to generate the correct skeleton screen in the process of page redirection, and supports the skeleton screen generation during local development. In the future, we will implement styles supporting the development of custom generated skeleton screen nodes and the generation of component skeleton screen, and optimize the algorithm of node filtering and processing in evaldom.js. Stay tuned!
Finally, we are looking for a senior front-end development engineer. Please send your resume to: [email protected].
Authors: Kang Cenbo, Sun Haonan, front end R&D engineer of Hornet’s Cell e-commerce platform.