Exploring front-end performance at the code level

768e8f7559376c5b4e7b87a4f5975638.gif

I. Introduction

I have been doing performance optimization recently, and the specific optimization methods are all over the Internet, so I won’t repeat them here.

Performance optimization can be divided into the following dimensions: code level, construction level, and network level.
This article mainly explores front-end performance from the code level, and is mainly divided into the following four sections.

  • Use CSS instead of JS

  • In-depth analysis of JS

  • Front-end algorithm

  • Computer bottom layer

2. Use CSS instead of JS s

Here we mainly introduce it from two aspects: animation and CSS components.

CSS animation

Before CSS2 came out, even a simple animation had to be implemented through JS. For example, the horizontal movement of the red square below:

99dc1666f87f127f26a9f042433ebd6b.gif

Corresponding JS code:

 
  
let redBox = document.getElementById('redBox')
let l = 10


setInterval(() => {
    l+=3
    redBox.style.left = `${l}px`
}, 50)

The 1998 CSS2 specification defined some animation properties, but due to limitations of browser technology at the time, these features were not widely supported and applied.

Until the introduction of CSS3, CSS animations were more fully supported. At the same time, CSS3 also introduces more animation effects, making CSS animation widely used in today's Web development.

So what animations can be achieved with CSS3, here are a few examples:

  • Transition - Transition is one of the commonly used animation effects in CSS3. By transforming certain attributes of an element, the element can smoothly transition from one state to another within a period of time.

  • Animation - Animation is another commonly used animation effect in CSS3. It is used to add some complex animation effects to an element. A series of animation sequences can be defined through keyframes (@keyframes).

  • Transform - Transform is a technology used in CSS3 to achieve 2D/3D graphics transformation effects, including rotation, scaling, movement, bevel and other effects.

Rewrite the above example into CSS code as follows:

 
  
#redBox {
    animation: mymove 5s infinite;
}


@keyframes mymove
{
    from {left: 0;}
    to {left: 200px;}
}

The same effect can be achieved using styles, so why not.

It should be pointed out that CSS animations are still developing and improving. With the emergence of new browser features and CSS versions, the features of CSS animations are constantly being added and optimized to meet increasingly complex animation needs and better user experience.

CSS components

In some well-known component libraries, most of the props of some components are implemented by modifying CSS styles, such as Vant's Space component.

Props

Function

CSS styles

direction

spacing direction

flex-direction: column;

align

Alignment

align-items: xxx;

fill

Whether to make Space a block-level element and fill the entire parent element

display: flex;

wrap

Whether to wrap lines automatically

flex-wrap: wrap;

Another example is Ant Design's Space component.

Props

Function

CSS styles

align

Alignment

align-items: xxx;

direction

spacing direction

flex-direction: column;

size

spacing size

gap: xxx;

wrap

Whether to wrap lines automatically

flex-wrap: wrap;

This type of component can be completely encapsulated into SCSS mixin implementation (the same is true for LESS), which can not only reduce the construction volume of the project (the size of the Space component of the two libraries after gzip is 5.4k and 22.9k respectively), but also improve performance.

To view the volume of a component in the component library, please visit the link: https://bundlephobia.com/.

For example, the following space mixin:

 
  
/* 
* 间距
* size: 间距大小,默认是 8px
* align: 对齐方式,默认是 center,可选 start、end、baseline、center
* direction: 间距方向,默认是 horizontal,可选 horizontal、vertical
* wrap: 是否自动换行,仅在 horizontal 时有效,默认是 false
*/
@mixin space($size: 8px, $direction: horizontal, $align: center, $wrap: false) {
    display: inline-flex;
    gap: $size;


    @if ($direction == 'vertical') {
        flex-direction: column;
    }


    @if ($align == 'center') {
        align-items: center;
    }


    @if ($align == 'start') {
        align-items: flex-start;
    }


    @if ($align == 'end') {
        align-items: flex-end;
    }


    @if ($align == 'baseline') {
        align-items: baseline;
    }


    @if ($wrap == true) {
        @if $direction == 'horizontal' {
            flex-wrap: wrap;
        }
    }
}

Similar components include Grid, Layout, etc.

Let’s talk about icons. Below is the first screenshot of the Ant Design icon component. There are many that can be easily implemented using only HTML + CSS.

Implementation ideas:

  • Prioritize implementation using only styles

  • If style alone is not enough, add a tag first and implement it through this tag and its two pseudo-elements::before and::after

  • If one label is not enough, consider adding additional labels.

For example, to implement a solid triangle that supports four directions, you can achieve it with just a few lines of styles (the screenshot above shows 4 icons):

 
  
/* 三角形 */
@mixin triangle($borderWidth: 10, $shapeColor: #666, $direction: up) {
    width: 0;
    height: 0;
    border: if(type-of($borderWidth) == 'number', #{$borderWidth} + 'px', #{$borderWidth}) solid transparent;


    $doubleBorderWidth: 2 * $borderWidth;
    
    $borderStyle: if(type-of($doubleBorderWidth) == 'number', #{$doubleBorderWidth} + 'px', #{$doubleBorderWidth}) solid #{$shapeColor};


    @if($direction == 'up') {
        border-bottom: $borderStyle;
    }


    @if($direction == 'down') {
        border-top: $borderStyle;
    }


    @if($direction == 'left') {
        border-right: $borderStyle;
    }


    @if($direction == 'right') {
        border-left: $borderStyle;
    }
}

In short, what can be implemented with CSS does not require JS. It not only has good performance, but also crosses technology stacks and even cross-terminals.

3. In-depth analysis of JS

After introducing CSS, let’s look at JS, mainly from two aspects: basic statements and framework source code.

Optimization of if-else statements

First understand how the CPU executes conditional statements. Refer to the following code:

 
  
const a = 2
const b = 10
let c
if (a > 3) {
    c = a + b
} else {
    c = 2 * a
}

The CPU execution flow is as follows:

bade69f3645f3e7c544c2b29c6f9a7c8.jpeg

We see that when instruction 0102 is executed, because the condition a > 3 is not met, it jumps directly to instruction 0104 for execution; moreover, the computer is very smart, if it finds during compilation that a can never be greater than 3 , it will directly delete the instruction 0103, and then the instruction 0104 will become the next instruction, which will be executed directly in sequence, which is the optimization of the compiler.

So back to the topic, if there is the following code:

 
  
function check(age, sex) {
    let msg = ''
    if (age > 18) {
        if (sex === 1) {
            msg = '符合条件'
        } else {
            msg = ' 不符合条件'
        }
    } else {
        msg = '不符合条件'
    }
}

The logic is very simple. It is to filter out people with age > 18 and sex == 1. There is no problem with the code at all, but it is too verbose. From the perspective of the CPU, two jump operations need to be performed. When age > 18 , enter the inner if-else to continue judging, which means jumping again.

In fact, we can directly optimize this logic (usually we do this, but we may know it but not know why):

 
  
function check(age, sex){
    if (age > 18 && sex ==1) return '符合条件'
    return '不符合条件'
}

Therefore, if the logic can end early, it will end early to reduce CPU jumps.

Optimization of Switch statement

In fact, there is not much difference between the switch statement and the if-else statement, except that they are written in different ways. However, the switch statement has a special optimization, that is, arrays.

Refer to the following code:

 
  
function getPrice(level) {
    if (level > 10) return 100
    if (level > 9) return 80
    if (level > 6) return 50
    if (level > 1) return 20
    return 10
}

We change it to a switch statement:

 
  
function getPrice(level) {
    switch(level)
        case 10: return 100
        case 9: return 80
        case 8: 
        case 7: 
        case 6: return 50
        case 5:
        case 4: 
        case 3:
        case 2: 
        case 1: return 20
        default: return 10
}

It looks like there is no difference, but in fact the compiler will optimize it into an array, where the subscripts of the array are from 0 to 10. The price corresponding to different subscripts is the value of return, that is:

3f722f34abf8c4de750c473f8681ef2e.jpeg

And we know that arrays support random access and are extremely fast. Therefore, the compiler's optimization of switch will greatly improve the running efficiency of the program, which is much faster than executing commands one by one.

So, what other if-else statements should I write? Can I just write all switches?

no! Because the compiler's optimization of switch is conditional, it requires that your code must be compact, that is, continuous.

Why is this? Because I want to use an array to optimize you. If you are not compact, for example, your code is 1, 50, 51, 101, 110, I will create an array with a length of 110 to store you. Only these positions are useful. , isn’t it a waste of space!

Therefore, when we use switch, we try to ensure that the code is of compact numeric type.

Optimization of loop statements

In fact, loop statements are similar to conditional statements, but they are written in different ways. The optimization point of loop statements is mainly to reduce instructions.

Let’s first look at how to write the second grade:

 
  
function findUserByName(users) {
   let user = null
   for (let i = 0; i < users.length; i++) {
       if (users[i].name === '张三') {
           user = users[i]
       }
   }
   return user
}

If the length of the array is 10086, and the first person is called Zhang San, then the next 10085 traversals will be in vain, and the CPU is really not used as a human being.

Couldn't you just write it like this:

 
  
function findUserByName(users) {
    for (let i = 0; i < users.length; i++) {
        if (users[i].name === '章三') return users[i]
    }
}

This is highly efficient in writing and highly readable, and it is also consistent with our above-mentioned logic that if it can end early, it will end early. CPU thanks you all directly.

In fact, there is something that can be optimized here, that is, the length of our array can be extracted without having to access it every time, that is:

 
  
function findUserByName(users) {
    let length = users.length
    for (let i = 0; i < length; i++) {
        if (users[i].name === '章三') return users[i]
    }
}

This may seem like a bit of a nitpick, and it is, but if you consider performance, it's still useful. For example, the size() function of some collections is not a simple attribute access, but needs to be calculated once every time. This scenario is a big optimization, because it saves the process of many function calls, which means saving There are many call and return instructions, which undoubtedly improves the efficiency of the code. Especially in the case of loop statements where quantitative changes easily lead to qualitative changes, the gap is widened from this detail.

Function calling process reference:

35dcbdadb481779be5c6c64864750580.jpeg

The corresponding code is as follows:

 
  
let a = 10
let b = 11


function sum (a, b) {
    return a + b
}

After talking about a few basic statements, let’s take a look inside the framework we often use. The performance in many places is worth exploring.

diff algorithm

Both Vue and React use virtual DOM. When performing updates, compare the old and new virtual DOM. Without any optimization, the time complexity of strictly diffing two trees directly is O(n^3), which is not usable at all. Therefore, Vue and React must use the diff algorithm to optimize the virtual DOM:

Vue2 - Double-ended comparison:

1b78881f75d8674f36f40c8eacfdc495.png

Similar to the picture above:

  • Define 4 variables: oldStartIdx, oldEndIdx, newStartIdx and newEndIdx

  • Determine whether oldStartIdx and newStartIdx are equal

  • Determine whether oldEndIdx and newEndIdx are equal

  • Determine whether oldStartIdx and newEndIdx are equal

  • Determine whether oldEndIdx and newStartIdx are equal

  • At the same time, oldStartIdx and newStartIdx move to the right; oldEndIdx and newEndIdx move to the left.

Vue3 - Longest increasing subsequence:

a427401f73a7d2ce7e299ef9dc8d4be9.png

The entire process is optimized again based on Vue2's double-ended comparison. For example, the screenshot above:

  • First perform a double-end comparison and find that the first two nodes (A and B) and the last node (G) are the same and do not need to be moved.

  • Find the longest increasing subsequence C, D, E (a group of nodes that includes both old and new children, and the longest order has not changed)

  • Treat the subsequence as a whole, without any internal operation. You only need to move F to the front of it and insert H to the back of it.

React - shift right only:

6dcceb45350df9f21c74b147c0e6f368.png

The comparison process of the above screenshot is as follows:

  • Traverse Old and save the corresponding subscript Map

  • Traversing New, the subscript of b changes from 1 to 0 and does not move (it is a left shift, not a right shift)

  • The subscript of c changes from 2 to 1, and does not move (it also moves to the left, not the right)

  • The subscript of a changes from 0 to 2, moving to the right, and the subscripts of b and c are reduced by 1.

  • The positions of d and e have not changed and do not need to be moved.

In short, no matter what algorithm is used, their principles are:

  • Only compare at the same level, not across levels

  • If the Tag is different, delete it and rebuild it (no longer compare internal details)

  • Child nodes are distinguished by key (importance of key)

In the end, the time complexity was successfully reduced to O(n) before it could be used in our actual projects.

Is setState really asynchronous?

Many people think setState is asynchronous, but look at the following example:

 
  
clickHandler = () => {
    console.log('--- start ---')


    Promise.resolve().then(() => console.log('promise then'))


    this.setState({val: 1}, () => {console.log('state...', this.state.val)})


    console.log('--- end ---')
}


render() {
    return <div onClick={this.clickHandler}>setState</div>
}

Actual printing result:

eb62cf3c584e661bd3ff2133149be841.jpeg

If it is asynchronous, the printing of state should be executed after the microtask Promise.

In order to explain this reason, we must first understand the event mechanism in JSX.

Events in JSX, such as onClick={() => {}}, are actually called synthetic events, which are different from the custom events we often call:

 
  
// 自定义事件
document.getElementById('app').addEventListener('click', () => {})

Synthetic events are bound to the root node and have pre- and post-operations. Take the above example:

 
  
function fn() { // fn 是合成事件函数,内部事件同步执行
    // 前置
    clickHandler()
    
    // 后置,执行 setState 的 callback
}

You can imagine that there is a function fn, and the events in it are executed synchronously, including setState. After fn is executed, the asynchronous event starts to be executed, that is, Promise.then, which is consistent with the printed result.

So why does React do this?

Because of performance considerations, if the state needs to be modified multiple times, React will merge these modifications first, and only render the DOM once after merging to avoid rendering the DOM every time it is modified.

Therefore, the essence of setState is synchronization, and the daily "asynchronous" is not rigorous.

4. Front-end algorithm

After talking about our daily development, let’s talk about the application of algorithms in the front-end.

Friendly reminder: Algorithms are generally designed for large data volumes, which are different from daily development.

If you can use value types, you don’t need reference types.

Let’s look at a question first.

Find all symmetric numbers between 1-10000, for example: 0, 1, 2, 11, 22, 101, 232, 1221...

Idea 1 - Use array reversal and comparison: convert numbers to strings, and then convert them to arrays; reverse arrays, and then join them into strings; compare the strings before and after.

 
  
function findPalindromeNumbers1(max) {
    const res = []
    if (max <= 0) return res


    for (let i = 1; i <= max; i++) {
        // 转换为字符串,转换为数组,再反转,比较
        const s = i.toString()
        if (s === s.split('').reverse().join('')) {
            res.push(i)
        }
    }


    return res
}

Idea 2 - String head and tail comparison: Convert numbers to strings; compare string head and tail characters.

 
  
function findPalindromeNumbers2(max) {
    const res = []
    if (max <= 0) return res


    for (let i = 1; i <= max; i++) {
        const s = i.toString()
        const length = s.length


        // 字符串头尾比较
        let flag = true
        let startIndex = 0 // 字符串开始
        let endIndex = length - 1 // 字符串结束
        while (startIndex < endIndex) {
            if (s[startIndex] !== s[endIndex]) {
                flag = false
                break
            } else {
                // 继续比较
                startIndex++
                endIndex--
            }
        }


        if (flag) res.push(res)
    }


    return res
}

Idea 3 - Generate flip numbers: Use % and Math.floor to generate flip numbers; compare the numbers before and after (operating numbers throughout, no string type).

 
  
function findPalindromeNumbers3(max) {
    const res = []
    if (max <= 0) return res


    for (let i = 1; i <= max; i++) {
        let n = i
        let rev = 0 // 存储翻转数


        // 生成翻转数
        while (n > 0) {
            rev = rev * 10 + n % 10
            n = Math.floor(n / 10)
        }


        if (i === rev) res.push(i)
    }


    return res
}

Performance Analysis: Getting Faster

  • Idea 1- It seems to be O(n), but array conversion and operation take time, so it is slow

  • Idea 2 VS Idea 3 - Manipulate numbers faster (the computer prototype is a calculator)

In short, try not to convert data structures, especially ordered structures such as arrays, and try not to use built-in APIs such as reverse. It is difficult to identify the complexity. Number operations are the fastest, followed by strings.

Try to use "low-level" code

Let’s go straight to the next question.

Enter a string and switch the uppercase and lowercase letters in it.
For example, input the string 12aBc34 and output the string 12AbC34.

Idea 1 - Use regular expressions.

 
  
function switchLetterCase(s) {
    let res = ''


    const length = s.length
    if (length === 0) return res


    const reg1 = /[a-z]
    const reg2 = /[A-Z]


    for (let i = 0; i < length; i++) {
        const c = s[i]
        if (reg1.test(c)) {
            res += c.toUpperCase()
        } else if (reg2.test(c)) {
            res += c.toLowerCase()
        } else {
            res += c
        }
    }


    return res
}

Idea 2 - Judge by ASCII code.

 
  
function switchLetterCase2(s) {
    let res = ''


    const length = s.length
    if (length === 0) return res


    for (let i = 0; i < length; i++) {
        const c = s[i]
        const code = c.charCodeAt(0)


        if (code >= 65 && code <= 90) {
            res += c.toLowerCase()
        } else if (code >= 97 && code <= 122) {
            res += c.toUpperCase()
        } else {
            res += c
        }
    }


    return res
}

Performance analysis: The former uses regularization and is slower than the latter

Therefore, try to use "low-level" code and use syntactic sugar, high-level APIs or regular expressions with caution.

5. Computer bottom layer

Finally, let’s talk about some of the underlying aspects of the computer that the front-end needs to understand.

Read data from "memory"

What we usually say: reading data from memory means reading data into registers. However, our data is not read directly from memory into registers, but is read into a cache first and then read into registers.

The register is within the CPU and is also part of the CPU, so the CPU reads and writes data from the register very quickly.

Why is this? Because reading data from memory is too slow.

You can understand it this way: the CPU first reads the data into the cache for use, and when it is actually used, it reads the register from the cache; when the register is used, it writes the data back to the cache, and then The cache then writes the data to memory at the appropriate time.

The CPU operation speed is very fast, but reading data from the memory is very slow. If you read and write data from the memory every time, it will inevitably slow down the CPU operation speed. It may take 100 seconds to execute, and 99 seconds will be spent reading data. In order to solve this problem, we put a cache between the CPU and the memory, and the read and write speed between the CPU and the cache is very fast. The CPU only reads and writes data to and from the cache, regardless of the cache and the cache. How to synchronize data between memories. This solves the problem of slow memory reading and writing.

Binary bit operations

Flexible use of binary bit operations can not only increase speed, but proficient use of binary can also save memory.

If a number n is given, how to determine whether n is 2 raised to the nth power?

It's very simple, just ask for the remainder.

 
  
function isPowerOfTwo(n) {
    if (n <= 0) return false
    let temp = n
    while (temp > 1) {
        if (temp % 2 != 0) return false
        temp /= 2
    }
    return true
}

Well, there’s nothing wrong with the code, but it’s not good enough. Take a look at the code below:

 
  
function isPowerOfTwo(n) {
    return (n > 0) && ((n & (n - 1)) == 0)
}

You can use console.time and console.timeEnd to compare the running speed.

We may also see that there are many flag variables in some source codes. We perform bitwise AND or bitwise OR operations on these flags to detect the flags and determine whether a certain function is enabled. Why doesn't he just use Boolean values? It's very simple, it's efficient and saves memory.

For example, this code in the Vue3 source code not only uses bitwise AND and bitwise OR, but also uses left shift:

 
  
export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}




if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
  ...
}




if (hasDynamicKeys) {
      patchFlag |= PatchFlags.FULL_PROPS
    } else {
    if (hasClassBinding) {
      patchFlag |= PatchFlags.CLASS
    }
    if (hasStyleBinding) {
      patchFlag |= PatchFlags.STYLE
    }
    if (dynamicPropNames.length) {
      patchFlag |= PatchFlags.PROPS
    }
    if (hasHydrationEventBinding) {
      patchFlag |= PatchFlags.HYDRATE_EVENTS
    }
}

6. Conclusion

The article explains the performance of the front-end from the code level, with in-depth dimensions:

  • In-depth analysis of JS basic knowledge

  • Framework source code

There are also breadth dimensions:

  • CSS animations, components

  • algorithm

  • Computer bottom layer

I hope it can help everyone broaden their horizons on front-end performance. If you are interested in the article, please leave a message for discussion.

-end-

Guess you like

Origin blog.csdn.net/jdcdev_/article/details/133533364