First screen optimization (code)

For common performance optimizations, such as anti-shake throttling, lazy loading, virtual lists, etc., what other optimizations can be made based on this?

Two sides of the goose factory:

  1. Have you heard about publish and subscribe? How else can you optimize...

  2. ...

  3. Have you heard about the virtual list? How else can it be optimized...

First Screen Optimization

In the current front-end development field, the front-end and back-end are separated, and the mainstream front-end frameworks are SPA and MPA; this means that page rendering and waiting for the white screen time have become problems that we need to solve; and large projects, this problem particularly prominent.

  • webpack can realize on-demand loading, reducing the code size that we need to load on the first screen;
  • @babel/plugin-syntax-dynamic-importPlugin that allows dynamic loading of code using the dynamic import syntax

  • Used to process JavaScript files and enable plugins inbabel-loader options@babel/plugin-syntax-dynamic-import

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: ['@babel/plugin-syntax-dynamic-import']
          }
        }
      }
    ]
  }
};

// 动态加载一个模块
import('./LazyComponent.vue').then(module => {
   this.showLazyComponent = true;
});
复制代码
  • Coupled with CDN and some static code (framework, component library, etc...) caching technology, this problem of long loading and rendering time can be well alleviated.

But even so, the loading of the first screen still has the problem of waiting time for loading and rendering;

Skeleton screen

Skeleton screen fills the page skeleton before the content appears, so as not to leave blank

Option One,

The skeleton screen is implemented in div#app in index.html, and the content of the div#app skeleton screen in index.html will be replaced after the program is rendered;

 

Solution 2: Use a Base64 image as the skeleton screen

The mobile page of Xiaomi Mall adopts this method, which uses a Base64 image as the skeleton screen.

According to plan one, write this Base64 image in div#app in our index.html module.

Solution 3: v-ifDynamically switch components to complete the skeleton screen


Anti-shake throttling and optimization

It is used to control the triggering frequency of events, reduce unnecessary calculations or requests, and improve page performance and user experience.

// 防抖是指在一定时间内,多次触发同一事件,只执行最后一次
// 节流是指在一定时间内,多次触发同一事件,只执行一次
function debounce_throttle(delay, immediate = false) { // 默认防抖
  let timer;
  if(immediate) { // 立即执行,节流,n秒执行一次
    return function(fn, ...args) {
      if(timer) return timer;
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  } else { // 非立即执行,防抖,n秒内重复点击只执行最后一次
    return function(fn, ...args) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, delay);
    }
  }
}

// 使用
const throttleScroll = debounce_throttle(200);
const fn = () => console.log('执行一波');
window.addEventListener('scroll', throttleScroll.bind(null, fn));
复制代码

Optimize anti-shake throttling

1. Use function caching to optimize performance (emphasis!!) . You can use the function cache to cache the execution results of the function to avoid repeated execution, thereby improving performance. ( that is, the computed attribute computed lazy calculation cache in vue )

You can also use LRU Cache to cache function execution results. LRU caches commonly used data and the fixed size will not cause excessive memory usage. It is not only used for anti-shake throttling

Please implement an LRU cache function? - Nuggets (juejin.cn)

// LRU 实现
class LRU {
    constructor(n) {
        this.size = n; // 初始化内存条
        this.cacheMap = new Map() // 新插入的数据排在后面,旧数据放在前面
    }
    put(domain, info) {
        this.cacheMap.has(domain) && this.cacheMap.delete(domain);
        this.cacheMap.size >= this.size && this.cacheMap.delete(this.cacheMap.keys().next().value);
        this.cacheMap.set(domain, info);
    }
    get(domain) {
        if(!this.cacheMap.has(domain)) return false;
        const info = this.cacheMap.get(domain);
        this.put(domain, info);
        return info;
    }
}

// 使用高阶函数,闭包隐藏私有变量_Cache
function memoize(func) {
  const _Cache = new LRU(50); // 缓存50个函数结果
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.get(key)) {
      return cache.get(key);
    } else {
      const result = func.apply(this, args);
      cache.set(key, result);
      return result;
    }
  }
}

// 使用 memoize 优化阶乘计算的性能
const factorial = memoize(function(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
});

console.log(factorial(5)); // 第一次计算,结果为 120
console.log(factorial(5)); // 直接从缓存中读取,结果为 120
复制代码
  1. Reasonably adjust the time interval according to the business scenario : generally set to 200ms
  2. Enable passive, the passive event listener allows the browser to automatically perform performance optimization during event processing , thereby improving page scrolling performance and response speed.
window.addEventListener('scroll', fn, {passive: true});
复制代码

Lazy loading and optimization

When the data enters the visible area and then loads

For example, for lazy loading of images, you can use the custom attribute [data-src], and when loading is required, get the custom attribute value and add the src path to img

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>

.container {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 20px;
}
.container img {
  max-width: 100%;
  height: auto;
  opacity: 0;
  transition: opacity 0.3s ease-in;
}
#aaa {
    width: 300px;
    height: 100vh;
    background-color: #f5f5f5;
}
    </style>
</head>
<body>
<div id="aaa">...</div>

<!-- 图片需要延迟加载的区域 -->
<div class="container">
    <img data-src="https://th.bing.com/th/id/OIP.WurMmN05Db16P84LH7MD9AHaEo?w=293&h=183&c=7&r=0&o=5&dpr=1.3&pid=1.7" alt="Image 1">
    <img data-src="https://th.bing.com/th/id/OIP.kB-Ovasi0GW67-rmwnAcwAHaEo?w=293&h=182&c=7&r=0&o=5&dpr=1.3&pid=1.7" alt="Image 2">
    <img data-src="https://th.bing.com/th/id/OIP.d0uSI7WjUmxhaR_MXssxRQHaE8?w=279&h=186&c=7&r=0&o=5&dpr=1.3&pid=1.7" alt="Image 3">
    <img data-src="https://th.bing.com/th/id/OIP.FRXTWwFuZjaNh8EWrdEmEAHaFS?w=231&h=180&c=7&r=0&o=5&dpr=1.3&pid=1.7" alt="Image 4">
</div>

<script>
// 获取所有需要延迟加载的图片元素
const images = document.querySelectorAll('[data-src]');

// 当前窗口可视区域高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;

// 加载图片
function loadImage(image) {
  const src = image.getAttribute('data-src');
  if (!src) return;
  image.src = src;
  image.removeAttribute('data-src');
  image.style.opacity = 1;
}

// 判断图片是否进入可视区域
function checkImage(image) {
  const rect = image.getBoundingClientRect();
  return rect.bottom >= 0 && rect.top < viewHeight;
}

// 懒加载函数
function lazyLoad() {
  for (let i = 0; i < images.length; i++) {
    if (checkImage(images[i])) {
      loadImage(images[i]);
    }
  }
}

// 滚动事件监听
function throttle() { // 首次执行
    let isThrottled = false;
    return function(fn) {
        if (!isThrottled) {
            isThrottled = true;
            setTimeout(() => {
                fn();
                isThrottled = false;
            }, 200); // 设置执行频率,200ms
        }
    }
}
const throttleScroll = throttle()
window.addEventListener('scroll', throttleScroll.bind(null, lazyLoad));

// 页面初次加载时执行一次懒加载
lazyLoad();

</script>
</body>
</html>
复制代码

Optimize lazy loading

  • Image preloading

Load the picture in advance in memory, and read it directly from the memory when using it

<!-- 图片预处理,在内存中进行提前加载-->
<script>
const imgList = [
"https://p6-passport.byteacctimg.com/img/user-avatar/fa2e0968fe519340b42fd210e696214c~300x300.image"
];
let count = 0;
imgList.forEach((img) => {
  let image = new Image();
  image.src = img;
  img.onload = () => {
    count++;
    if(count === imgList.length) console.log('所有图片都预加载完成');
  }
})
</script>
复制代码
  • Anti-shake throttling
  • For image loading, you can use http2, because the multiplexing of http2 can request resources concurrently without blocking other resource requests

Publish subscribe and optimize

Node's Event module

// 简单实现发布订阅
// 1. 同一名称事件可能有多个不同的执行函数
// 2. 通过"on"函数添加事件
// 3. 通过"emit"函数触发事件
class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(event, fn) {
        if(!this.events[event]) this.events[event] = [];
        this.events[event].push(fn);
    }
    emit(event) {
        this.events[event].forEach(fn => fn());
    }
}
const Event = new EventEmitter();
Event.on('call', () => {
    console.log('call');
})
Event.emit('call');
复制代码

optimization

1. Use the event queue (emphasis!!)

For example, vue's nextTick is updated uniformly in the next event loop

{
    
    {i}}

{i: 0};
for(let index = 0; index < 1000; index++) i++;
复制代码

Vue will hijack data (vue's Scheduler scheduler). If there is no event queue optimization, 1000 dom updates will be required, which will undoubtedly cause serious performance problems . Using nextTick optimization, it will only be executed in the next event loop The last operation, that is, only dom once, will only be executedi++ --> 1000

Expected implementation: the following code will only trigger set once

const obj = new Proxy({i: 1}, {
    set(...args) {
        console.log('触发')
        return Reflect.set(...args);
    }
})
for(let i = 0; i < 100; i++) {
    obj.i++;
}
// 触发
复制代码

Implemented using Promise microqueues and event queues:

For the event queue, the stored value should be an object, including the id value (monitored attribute) and the run callback function

That is to implement the Component class, monitor the attribute value of each Porxy object, such as the previous one obj.i++;, which needs to be merged into the last operation

{id: any, run: Function}

Implementation of merging deduplication queues (deduplication of objects with the same id)

const arr = [{id: 1, run: ()=>{}}, {id: 1, run: ()=>{}}, {id: 2, run: ()=>{}}];

const result = Array.from(new Set(arr.map(item => item.id)))
    .map(id => arr.find(item => item.id === id));

console.log(result); // 输出:[{id: 1}, {id: 2}]
复制代码

The final realization:

// 单例模式,实例化同一个类,对于同个对象只会触发第一次new,这里用来监听一个对象数据
class Component {
    constructor() {
        this.callbacks = [];
        this.pending = false;
        this.nextTickHandler = () => { // 添加入微任务队列执行
            Promise.resolve().then(this.runCallbacks);
        };
        this.runCallbacks = () => { // 执行回调
            this.pending = false;
            const copies = this.callbacks.slice(0);
            this.callbacks.length = 0;
            // 去重事件并执行
            Array.from(new Set(copies.map(item => item.id)))
                .map(id => copies.find(item => item.id === id)).forEach(watcher => watcher.run());
        };
    }
    nextTick(callback, key) {
        this.callbacks.push({id: key, run: callback});
        if (!this.pending) {
            this.pending = true;
            this.nextTickHandler();
        }
    }
}
const monitor = singleton(Component) // 设计单例
function singleton(className) {
    let ins; // 隐藏私有变量
    return new Proxy(className, {
        // handler.construct() 方法用于拦截 new 操作符
        construct(target, ...args) {
            if(!ins) ins = new target(...args);
            return ins;
        }
    })
}
  
const obj = new Proxy({i: 1}, {
    set(target, key, value, receiver) {
      const _monitor = new monitor(); // 设计单例监听一个对象
      _monitor.nextTick(() => { // 模拟操作dom
        console.log('触发');
      }, key)
      return Reflect.set(target, key, value, receiver);
    }
});
  
for (let i = 0; i < 100; i++) {
    obj.i++;
}
复制代码
  1. Use asynchronous event handlers to avoid blocking the main thread

Use setTimeout, requestAnimationFrame, etc.

  1. Use event delegation to avoid registering events on each element
<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<script>
ul.onclick = function(e) {
  if(e.target.tagName === 'LI') {
     console.log(e.target.textContent);
  }
}
</script>
复制代码
  1. Cache event results (as mentioned earlier) or cache event handler functions

Vue 3 introduces the optimization of caching event handlers, which can reduce the creation of new event handler instances every time rendering, thereby improving performance. The specific implementation is to cache event handlers and reuse them when rendering.

  • Make sure only necessary events are registered and fired

Virtual listings and optimization

The virtual list is a technology to optimize the display of a large amount of data in the UI interface. The basic idea is to only display the data in the visible area on the screen instead of loading all the data into the memory. As the user scrolls, the system dynamically loads new data and unloads data that has left the screen.

overflow: scrollTo hide the data, use splice to dynamically cut the array data that needs to be displayed

  1. Calculate the height of the list: First, you need to calculate the height of the entire list in order to calculate the size of the visible area.
  2. Calculate the viewable area: Calculate the size and position of the viewable area based on the current list height and screen height.
  3. Render the visible area: According to the position and size of the visible area, render all the elements in the visible area.
  4. Listen to scrolling events: When the user scrolls, calculate the position and size of the new visible area by listening to scrolling events.
  5. Dynamic loading of data: When the visible area changes, dynamically load new data by calculating the data that needs to be loaded in the visible area.
  6. Unload data that has left: At the same time, data that has left the visible area needs to be unloaded to save memory space.
<!-- 简单实现虚拟列表 -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>虚拟滚动列表</title>
    <script src="https://cdn.staticfile.org/vue/2.7.0/vue.min.js"></script>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      .container {
        width: 50%;
        overflow-y: auto;
        box-sizing: border-box;
        border: 1px solid black;
      }
      .list-item {
        list-style: none;
        border: 1px solid red;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 外层盒子,固定高度 -->
      <div class="container" :style="`height: ${boxH}px`" @scroll="handleScroll">
        <!-- 内层滚动盒子, 高度是虚拟的数据的整体高度!!!, 这样使得滚动条更像真实的 -->
        <div class="scroll-box" :style="`height: ${allHeight}px`">
          <!-- 真正显示区域, 需要通过trasform 来让显示区域一直在屏幕中,而不是滑走 -->
          <ul :style="`transform:translateY(${offsetY}px)`">
            <li
              v-for="item in nowList" :key="item"
              :style="`height: ${itemH}px`"
              class="list-item"
              >{
   
   {item}}</li>
          </ul>
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el:"#app",
        data() {
          return {
            boxH: 700, // 外层盒子高度
            itemH: 100, // 每个元素的高度
            ListNum: 100, // 整体个数
            list: [], // 列表整体数据
            nowList: [], // 目前显示列表
            offsetY: 0, // 显示区域动态偏移量
          }
        },
        created() {
          // 初始化第一页面的数据
          this.init()
        },
        computed: {
          allHeight() {
            return this.ListNum * this.itemH
          },
          pageNum() {
            return Math.ceil(this.boxH / this.itemH)
          }
        },
        methods: {
          init() {
            // 1. 模拟整个列表元素
            const list = []
            for(let i = 0; i < this.ListNum; i++) {
              list.push(i)
            }
            this.list = list
            // 2. 取得当前第一页的显示数据
            this.nowList = this.list.slice(0, this.pageNum + 1) // 注意高度卷起一半
          },
          handleScroll(e) {
            // e.target.scrollTop 卷起高度, 需注意滑动单个元素显示一半的情况
            const scrollTop = e.target.scrollTop
            // 1.保持显示区域一直在屏幕上
            this.offsetY = scrollTop - (scrollTop % this.itemH)

            // 2.计算卷起多少个,替换更新
            let startIndex = Math.floor(scrollTop / this.itemH)
            this.nowList = this.list.slice(startIndex, startIndex + this.pageNum)
          }
        }
      })
    </script>
  </body>
</html>
复制代码

optimization

  • Cache List Items

More list items up and down are used to load the cache

For example, if there are 10 items in the display list, 30 items can be loaded at one time, and 10 more items are used as a cache. Slide the scroll bar to load more than 10 items to load new content

  • paging

  • Anti-shake throttling

// 滚动用节流,也就是立即执行
handleScroll(e) {
  // 记录变量lastUpdateTime表示上次最后滚动的时间
  if(Date.now() - this.lastUpdateTime <= 100) return;
  ...
  // 更新上一次刷新时间
  this.lastUpdateTime = Date.now()
}

Guess you like

Origin blog.csdn.net/a1014981613/article/details/130286073