第七章:JavaScript性能优化实战

JavaScript性能直接关乎网页交互体验,优化工作涵盖代码结构规整、执行效率提升以及内存管理等方面,对提升页面流畅度极为关键。

7.1 优化代码结构

7.1.1 函数节流与防抖

在处理频繁触发的事件时,函数节流(Throttle)和防抖(Debounce)能有效减少不必要的函数调用,提升性能。

  • 函数节流:设定一个固定的时间周期,在这个周期内,无论事件触发多少次,函数都只执行一次。

  • 错误示范:在窗口滚动事件中实时获取滚动位置并执行复杂计算,没有使用节流函数,导致性能问题。

    window.addEventListener('scroll', () => {
    // 执行复杂计算,如获取滚动位置并实时更新大量DOM元素
    const scrollTop = window.pageYOffset;
    const elements = document.getElementsByTagName('div');
    for (let i = 0; i < elements.length; i++) {
    elements[i].style.transform = translateY(${scrollTop}px);
    }
    });

  • 正确示例:使用节流函数限制计算频率。

    function throttle(func, limit) {
    let inThrottle;
    return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
    func.apply(context, args);
    inThrottle = true;
    setTimeout(() => inThrottle = false, limit);
    }
    };
    }

    window.addEventListener('scroll', throttle(() => {
    const scrollTop = window.pageYOffset;
    const targetElement = document.getElementById('target - element');
    targetElement.style.transform = translateY(${scrollTop}px);
    }, 200));

  • 函数防抖:在事件触发后,等待一定时间,如果在这段时间内事件再次触发,则重新计时,直到等待时间结束后才执行函数。

  • 错误示范:在搜索框输入事件中,未使用防抖函数,导致用户每次输入都会立即发送搜索请求,造成网络资源浪费和性能问题。

    <script> function search() { const keyword = document.getElementById('search - input').value; // 发送搜索请求 console.log('搜索关键词:', keyword); } </script>
  • 正确示例:使用防抖函数,用户输入结束后再执行搜索请求。

    function debounce(func, delay) {
    let timer;
    return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => func.apply(context, args), delay);
    };
    }

    const searchInput = document.getElementById('search - input');
    searchInput.addEventListener('input', debounce(() => {
    const keyword = searchInput.value;
    // 发送搜索请求
    console.log('搜索关键词:', keyword);
    }, 300));

7.1.2 模块化与代码拆分

将代码拆分成独立的模块,不仅提高代码的可维护性和复用性,还能优化加载性能。在大型项目中,使用ES6模块系统或构建工具(如Webpack)进行代码拆分。

  • ES6模块:通过 import 和 export 语句,将不同功能的代码封装在独立文件中。

  • 错误示范:将所有代码写在一个文件中,没有进行模块化,导致代码混乱,难以维护。

    // 所有代码混合在一个文件中
    function add(a, b) {
    return a + b;
    }

    function subtract(a, b) {
    return a - b;
    }

    // 其他大量功能代码

    • 正确示例:将工具函数封装在 utils.js 文件中,通过 import 和 export 进行模块化管理。

    // utils.js
    export function add(a, b) {
    return a + b;
    }

    export function subtract(a, b) {
    return a - b;
    }

    // main.js
    import { add, subtract } from './utils.js';
    const result1 = add(1, 2);
    const result2 = subtract(5, 3);

  • Webpack代码拆分:利用Webpack的 splitChunks 配置,可以将公共代码和异步加载的模块拆分出来,实现按需加载。

  • 错误示范:没有配置Webpack代码拆分,所有代码打包在一个文件中,导致初始加载文件过大。

    // webpack.config.js,没有配置代码拆分
    module.exports = {
    //...
    };

  • 正确示例:配置 splitChunks 将公共代码和异步加载的模块拆分出来。

    // webpack.config.js
    module.exports = {
    //...
    optimization: {
    splitChunks: {
    chunks: 'all'
    }
    }
    };

7.2 提升执行效率

7.2.1 避免全局变量

全局变量会增加命名空间冲突的风险,同时影响垃圾回收机制的效率。尽量将变量定义在函数内部或模块作用域内。

  • 错误示例:

    let globalVar;
    function init() {
    globalVar = 10;
    //...
    }

  • 正确示例:

    function init() {
    const localVar = 10;
    //...
    }

7.2.2 优化循环操作

在循环中,减少不必要的计算和DOM操作,缓存循环长度,避免每次循环都进行计算。

  • 优化前:

    const list = document.getElementById('my - list');
    const items = list.getElementsByTagName('li');
    for (let i = 0; i < items.length; i++) {
    items[i].style.color ='red';
    // 其他复杂计算,如每次循环都重新计算一个复杂的数学表达式
    const complexResult = Math.sqrt(i * i + 10) * Math.sin(i);
    console.log(complexResult);
    }

  • 优化后:

    const list = document.getElementById('my - list');
    const items = list.getElementsByTagName('li');
    const len = items.length;
    for (let i = 0; i < len; i++) {
    items[i].style.color ='red';
    }

7.3 内存管理

7.3.1 理解垃圾回收机制

JavaScript采用自动垃圾回收机制,通过标记清除(Mark - and - Sweep)算法回收不再使用的内存。当一个对象不再被任何变量引用时,它就会被标记为可回收,垃圾回收器会在适当的时候回收其占用的内存。

7.3.2 避免内存泄漏

内存泄漏是指不再使用的内存没有被及时回收,导致内存占用不断增加。闭包是导致内存泄漏的常见原因之一,下面着重介绍闭包相关的内存泄漏场景及解决办法。

  • 闭包导致内存泄漏的原理:闭包是指函数可以访问并操作其外部作用域的变量,即使外部函数已经执行完毕。如果闭包引用的外部变量在闭包外不再使用,但闭包仍然存在,会导致该变量无法被回收。

  • 错误示例1:事件监听器中的闭包内存泄漏

    const element = document.getElementById('myElement');
    function setupClickListener() {
    const largeData = new Array(1000000).fill(1);
    element.addEventListener('click', function clickHandler() {
    // clickHandler 形成闭包,largeData无法被回收
    console.log('Element clicked');
    });
    }
    setupClickListener();

在这个例子中, clickHandler 函数形成了闭包,它引用了 setupClickListener 函数作用域内的 largeData 。即使 setupClickListener 函数执行完毕, largeData 不再被外部使用,但由于 clickHandler 仍然存在(作为事件监听器), largeData 无法被垃圾回收,从而导致内存泄漏。

  • 错误示例2:返回函数形成的闭包内存泄漏

    function outer() {
    const data = {
    value: new Array(500000).fill(42)
    };
    return function inner() {
    // inner 函数形成闭包,data无法被回收
    return data.value;
    };
    }
    const closureFunction = outer();
    // 后续代码中,outer 函数作用域内的 data 不再被外部使用,但因闭包存在无法回收

这里 outer 函数返回的 inner 函数形成闭包,持续引用 data 对象。即使 outer 函数执行结束, data 在外部不再被使用,可因为 inner 函数的存在, data 不能被垃圾回收,造成内存泄漏。

  • 解决办法:

  • 及时移除事件监听器:在不需要事件监听器时,使用 removeEventListener 方法移除它,从而解除闭包对外部变量的引用。

    const element = document.getElementById('myElement');
    function setupClickListener() {
    const largeData = new Array(1000000).fill(1);
    const clickHandler = function() {
    console.log('Element clicked');
    };
    element.addEventListener('click', clickHandler);
    // 假设在某个条件下需要移除事件监听器
    setTimeout(() => {
    element.removeEventListener('click', clickHandler);
    // 此时 clickHandler 不再被引用,largeData 可以被回收
    }, 5000);
    }
    setupClickListener();

  • 手动解除闭包引用:如果闭包不再需要使用,可以将闭包赋值为 null ,强制解除对外部变量的引用。

    function outer() {
    const data = {
    value: new Array(500000).fill(42)
    };
    return function inner() {
    return data.value;
    };
    }
    let closureFunction = outer();
    // 使用闭包
    closureFunction();
    // 不再需要闭包时
    closureFunction = null;
    // 此时 data 可以被垃圾回收

7.3.3 如何查看内存泄漏

  • Chrome DevTools:

  • 使用"Memory"面板拍摄快照对比:打开开发者工具的"Memory"面板,先点击"Take snapshot"获取页面初始状态的内存快照。然后进行一系列操作,比如频繁触发可能导致内存泄漏的函数或事件。操作完成后,再次点击"Take snapshot"获取新的内存快照。在快照对比界面,可以查看对象的数量变化,若某些对象数量持续增加且无合理原因,可能存在内存泄漏。例如在上述闭包导致内存泄漏的代码示例中,多次触发点击事件后,对比快照会发现 largeData 相关对象没有减少,反而不断增多。

  • 使用"Record"功能记录内存变化:点击"Record"开始记录内存使用情况,在记录期间进行操作。完成操作后,点击"Stop"停止记录,此时会生成一个时间轴,展示内存使用随时间的变化趋势。如果在操作过程中内存持续上升且没有回落,可能存在内存泄漏。

  • Performance API:通过 performance.memory 属性可以获取内存相关信息,包括当前的堆内存使用量等。可以在代码中合适的位置插入获取内存信息的代码,比如在某个函数执行前后,对比内存使用量的变化。

    console.log('函数执行前内存使用量:', performance.memory.usedJSHeapSize);
    // 执行可能导致内存泄漏的函数
    someFunctionThatMayLeakMemory();
    console.log('函数执行后内存使用量:', performance.memory.usedJSHeapSize);

如果执行后内存使用量大幅增加且后续没有下降,就需要进一步排查是否存在内存泄漏。

  • Node.js环境下的排查工具:

  • 使用 heapdump 模块:安装 heapdump 模块后,在代码中合适位置插入 heapdump.writeSnapshot() 方法来生成堆内存快照文件。例如:

    const heapdump = require('heapdump');
    // 执行一些操作后生成快照
    setTimeout(() => {
    heapdump.writeSnapshot();
    }, 5000);

然后使用 node - heapdump 等工具分析生成的快照文件,查看对象的引用关系和内存占用情况,找出可能导致内存泄漏的对象。

  • 借助 process.memoryUsage() 方法:该方法可以获取当前Node.js进程的内存使用信息,包括RSS(resident set size,进程占用的物理内存大小)、heapTotal(V8堆的总大小)和heapUsed(V8堆中已使用的大小)等。可以在代码中定期调用该方法,观察内存使用趋势,判断是否有内存泄漏。

    setInterval(() => {
    const memoryUsage = process.memoryUsage();
    console.log('RSS:', memoryUsage.rss);
    console.log('Heap Total:', memoryUsage.heapTotal);
    console.log('Heap Used:', memoryUsage.heapUsed);
    }, 1000);

通过这些方法,可以有效避免闭包导致的内存泄漏,提升JavaScript代码的内存使用效率,进而优化整体性能。

相关推荐
敢嗣先锋5 小时前
鸿蒙5.0实战案例:基于ArkUI启动冷启动过程最大连续丢帧数问题分析思路&案例
性能优化·移动开发·多线程·harmonyos·arkui·鸿蒙开发
小塵11 小时前
【MySQL 优化】什么是回表?什么是索引覆盖?
后端·mysql·性能优化
拥有一颗学徒的心1 天前
鸿蒙第三方库MMKV源码学习笔记
笔记·学习·性能优化·harmonyos
独泪了无痕1 天前
MySQL查询优化-distinct
后端·mysql·性能优化
纯爱掌门人1 天前
鸿蒙Next复杂列表性能优化:让滑动体验如丝般顺滑
前端·性能优化·harmonyos
Eamonno1 天前
深入理解React性能优化:掌握useCallback与useMemo的黄金法则
react.js·性能优化
来恩10031 天前
PHP 性能优化全攻略:提升 Web 应用速度的关键
前端·性能优化·php
hello_simon1 天前
pdf转换成word在线 简单好用 支持批量转换 效率高 100%还原
性能优化·pdf·产品运营·word·pdf转换·自媒体·pdf转word
CoLiuRs2 天前
微服务监控与Go服务性能分析
网络·微服务·性能优化·golang