前端面试|加载优化、内存处理

667 阅读7分钟

本文笔记内容来自视频:爪哇教育 2021大厂前端核心面试题详解(二)-路白
跟着视频敲一遍,加强记忆,整理,方便复盘
手动搬砖,有不对的地方欢迎大佬指出

一、有做过前端加载优化相关的工作吗?都做过哪些努力。

做性能优化的目的是什么?

  1. 首屏时间
  2. 首次可交互时间
  3. 首次有意义内容渲染时间

常见的优化手段?

  1. 只请求当前需要的资源
    • 异步加载、懒加载、polyfill的优化
  2. 缩减资源体积
  3. 时序优化
    • js promise.all 并发发送请求
    • ssr 把打包放在服务端,由服务端做渲染输出,方便做不同缓存;不是放在CDN 上;方便seo
    • prefetch, prerender, preload
// 加载遇到xxx.com时立即进行dns的预解析
<link rel="dns-prefetch" href="xxx1.com" />
<link rel="dns-prefetch" href="xxx2.com" />
<link rel="preconnect" href="xxx1.com" />
// 预加载图片资源
<link rel="preload" as="image" href="https://aaa.com/xxx.png" />
  1. 合理利用缓存
    • cdn cdn预热 cdn刷新
    • http缓存
    • localStorage, sessionStorage

思考题🤔:如果一段js执行时间长,怎么去分析?(装饰器计算函数执行时间)

// 装饰器decorator
export function measure(target: any, name: string, descriptor: any) {
    const oldValue = descriptor.value;
    
    descriptor.value = async function () {
        console.time(name);
        const ret = await oldValue.apply(this, arguments);
        console.timeEnd(name);
        return ret;
    }
    return descriptor;
}

// 验证
export default class Home extends Vue {
    public longTimefn(){
        return new Promise((resolve) => setTimeout(resove, 3000));
    }
    // 通过measure 可以知道在控制台created输出执行多长时间
    @measure
    public async created(){
        await this.longTimefn();
    }
} 

思考题🤔:阿里云oss支持通过链接后面拼参数来做图片的格式转换,尝试写一下,把任意图片格式转换为webp,需要注意什么?

  • 判断浏览器是否兼容webp:caniuse.com/ & webp格式转换
  • 注意考虑一些边界问题
    function checkWebp() {
       try {
          return (
              document.createElement('canvas')//创建canvas元素,可把图片转成base64格式(base64开头是带图片格式的)
              .toDataURL('image/webp')
              .indexOf('data:image/webp') === 0 
          ) // 判断浏览器是否支持webp
       } catch (e) {
           return false;
       }
    }
    
    const supportWebp = checkWebp();
    
    export function getWebpImageUrl(url) {
        if(!url){
            throw Error('url 不能为空')
        }
        // 是否是base64格式
        if(url.startsWith('data:')){
            return url;
        }
        //是否支持webp
        if(!supportWebp) {
            return url;
        }
        return url + '?x-oss-processxxxx';//进行字符串拼接
    }

思考题🤔:如果有巨量的图片需要展示,除了懒加载的方式,有没有其他方法限制一下同时加载图片数量? (实质:代码题,实现promise并发控制)

  • Promise.race()返回第一个完成的结果,结果可以是resolves,也可以是rejects
function limitLoad(urls, handler, limit){
    const sequence = [].concat(urls);
    let promises = [];
    
    promises = sequence.splice(0, limit).map((url, index) => {
        return handler(url).then(()=>{
            retuen index;
        })
    });
    
    let p = Promise.race(promises);
    for (let i = 0; i < sequence.length; i++) {
        p = p.then((res) => {
            promises[res] = handler(sequence[i]).then(() => {
                return res;
            });
            return Promise.race(promises);
        })//链式完成顺序推入
    }
}

二、平时有关注过前端的内存处理吗?

1. 你了解js中的内存管理吗?什么情况会导致内存泄露?

  1. 内存的生命周期

    • 内存分配:声明变量,函数,对象的时候,系统会自动分配内存。
    • 内存使用:即读写内存,也就是调用,使用变量、函数等的时候。
    • 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存。
  2. Js中的内存分配

     const n = 123; // 给数值变量分配内存
     const s = “azerty”; // 给字符串分配内存
     const o = {
      a: 1,
      b: null
     }; // 给对象及其包含的值分配内存
    
  3. Js中的内存使用
    使用值的过程实际上是对分配内存进行读取与写入操作。读取与写⼊可能是写⼊⼀个变量或者⼀个对象的属性值,甚⾄传递函数的参数。

    var a = 10; // 分配内存
    console.log(a); // 对内存的使⽤
    
  4. js中的垃圾回收机制

    垃圾回收算法主要依赖于引用的概念。

    在内存管理的环境中,⼀个对象如果有访问另⼀个对象的权限(隐式或者显式),叫做⼀个对象引⽤另⼀个对象。

    例如:⼀个Javascript对象具有对它原型的引⽤(隐式引⽤)和对它属性的引⽤(显式引⽤) 在这⾥,“对象”的概念不仅特指 JavaScript 对象,还包括函数作⽤域(或者全局词法作⽤域)。

  • 4.1 引用计数算法。垃圾回收。缺陷:循环引用,内存泄露
    • 引⽤计数算法定义“内存不再使⽤”的标准很简单,就是看⼀个对象是否有指向它的引⽤。 如果没有其他对象指向它了,说明该对象已经不再需了。
    • 但它却存在⼀个致命的问题:循环引⽤。
    • 如果两个对象相互引⽤,尽管他们已不再使⽤,垃圾回收不会进⾏回收,导致内存泄露。
  • 4.2 标记清除算法。
    • 标记清除算法将“不再使⽤的对象”定义为“⽆法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使⽤的。 那些⽆法由根部出发触及到的对象被标记为不再使⽤,稍后进⾏回收。
      1. 在运行的时候给存储在内存的所有变量加上标记;
      2. 从根部触发,能触及的对象,把标记清除;
      3. 那些还存在有标记的就被视为将要删除的变量;
      4. 最后垃圾收集器会执⾏最后⼀步内存清除的⼯作,销毁那些带标记的值并回收它们所占⽤的内存空间。
  1. js中,有哪些常见的内存泄露?
    • 全局变量
    function foo() {
    bar1 = ‘some text’; // 没有声明变量 实际上是全局变量 => window.bar1
    this.bar2 = ‘some text’ // 全局变量 => window.bar2
    }
    foo();
    
    window.bar1 = null;//不用之后一定释放内存
    
    • 未被清除的定时器和回调 如果后续 renderer 元素被移除,整个定时器实际上没有任何作⽤。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器⽆法被内存回收, 定时器函数中的依赖也⽆法回收。在这个案例中的 serverData 也⽆法被回收。
    var serverData = loadData();
    setInterval(function() {
        var renderer = document.getElementById(‘renderer’);
        if(renderer) {
         renderer.innerHTML = JSON.stringify(serverData);
        }
    }, 5000); // 每 5 秒调⽤⼀次
    
    //清除定时器
    clearTimeout();
    clearInterval();
    
    • 闭包 一个内部函数,有权访问包含其的外部函数中的变量。下面这种情况下,闭包也会造成内存泄露
    var theThing = null;
    var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
        if (originalThing) // 对于 ‘originalThing’的引⽤
          console.log(“hi”);
      };
      theThing = {
        longStr: new Array(1000000).join(‘*’),
        someMethod: function () {
          console.log(“message”);
        }
      };
    };
    setInterval(replaceThing, 1000);
    
    这段代码,每次调⽤ replaceThing 时,theThing 获得了包含⼀个巨⼤的数组和⼀个对于新闭包 someMethod 的对象。 同时 unused 是⼀个引⽤了 originalThing 的闭包。这个范例的关键在于,闭包之间是共享作⽤域的,尽管 unused 可能⼀直没有被调⽤,但是someMethod 可能会被调⽤,就会导致⽆法对其内存进⾏回收。 当这段代码被反复执⾏时,内存会持续增⻓。
    • DOM的引用 很多时候,我们对DOM的操作,会把DOM的引用保存在一个数组或者Map中。 下面的案例中,即使我们对于image元素进行了移除,但是仍然有对image元素的引用,依然无法对其进行垃圾回收。此时,elements.image = null;就很有必要了
     const elements = {
         image: document.getElementById('image‘)
     }
     document.body.removeChild(document.getElementById('image‘));
     elements.image = null;//对象的属性仍然存在,记得释放
    
  2. 如何避免内存泄露
    • 减少不必要的全局变量,使⽤严格模式避免意外创建全局变量。
    • 使用完数据后,及时解除引用(闭包中的变量,dom引⽤,定时器清除)
    • 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

思考题🤔:实现sizeOf函数,传入一个参数object,计算这个object占用了多少bytes?

考察:1. 对于计算机基础,js内存基础的考察; 2. 递归; 3. 细心程度

  • number: 64位存储,8个字节
  • string: 每个长度2字节
  • boolean: 4字节

const xxx = {};

const testData = {
    a: 111, // 8字节 + key:2字节 = 10 
    b: 'aaa', // 6字节 + key:2字节 = 8
    2222: false, // 4字节 + key:2字节 = 6
    c: xxx,//注意:相同引用的内存是不占用额外的内存空间的 2+2 = 4
    d: xxx // 2字节 + 2 = 4
}//共32字节

const seen = new WeakSet();

// 对对象的处理
function sizeOfObject(object) {
    if(object === null){
        return 0;
    }
    let bytes = 0;
    // 注意:对象里的key也是占用内存空间的!
    const properties = Object.keys(object);
    for(let i = 0; i < properties.length; i++){
        const key = properties[i];
        bytes += calculator(key);//无论是什么类型,key都应该算上!
        
        if(typeof object[key] === 'object' && object[key] !== null) {
            if(seen.has(object[key])){
                continue;// 有坑
            }
            seen.add(object[key]);
        }
        
        // bytes += calculator(key);// 有坑
        bytes += calculator(object[key]);
    }
}

function calculator(object) {
    //判断类型,还可以:Object.prototype.toString.call(object) === '[object Object]'
    const objectType = typeof object;
    
    switch(objectType){
        case 'string':{
            return object.length * 2;
        }
        case 'boolean':{
            return 4;
        }
        case 'number':{
            return 8;
        }
        case 'object':{
            if(Array.isArray(object)) {
                //对数组的处理,递归处理
                // [1,2,3,4]
                // [{a:1},{b:2}]
                // 通过map把每个元素都经过calculator再处理一次,一直递归到不是object类型为止,再通过reduce做加法处理。
                return object.map(calculator).reduce((res,current) => res + current,0)
            
            }else{
                // 对对象的处理
                return sizeOfObject(object);
            }
        }
        
    }
    
}

console.log(calculator(testData));//32

参考自 object-sizeof