备战前端面试—工具与优化篇

Webpack

  1. 简述一下 webpack 的原理?

    webpack 是一个用于现代JavaScript应用程序的静态模块打包工具。

    webpack 处理应用程序时,会先从入口文件(通过entry指定)开始,递归查询依赖(这个过程使用 babel 来遍历 AST 抽象语法树,提取其中的 import 声明)。

    随后生成依赖图谱(使用了图这种数据结构,把各个模块的依赖推入数组)。

    webpack 将各个模块的代码封装到函数(seal)中,使用内部的一些工具函数处理依赖关系、上下文。

    实现一个加载方法将封装各个模块的函数引入(chunk),置于同一个执行环境。

    执行加载方法,准备输出产物。

    确认好输出内容后,根据配置输出的路径和文件名,把文件内容写入到文件系统。

  2. 说说 webpack 的常用配置?

    module.exports = {
        // __dirname值为所在文件的目录,context默认为执行webpack命令所在的目录
        context: path.resolve(__dirname, 'app'),
        // 必填项,编译入口,webpack启动会从配置文件开始解析,如下三种(还有一种动态加载entry的方式就是给entry传入一个函数,这个在项目比较大,页面很多的情况下可以优化编译时间)
        entry: './app/entry', // 只有一个入口,入口只有一个文件
        entry: ['./app/entry1', './app/entry2'], // 只有一个入口,入口有两个文件
           // 两个入口
        entry: {
            entry1: './app/entry1',
            entry2: './app/entry2'
        },
        // 输出文件配置
        output: {
            // 输出文件存放的目录,必须是string类型的绝对路径
            path: path.resolve(__dirname, 'dist'),
            // 输出文件的名称
            filename: 'bundle.js',
            filename: '[name].js', // 配置了多个入口entry是[name]的值会被入口的key值替换,此处输出文件会输出的文件名为entry1.js和entry2.js
            filename: [chunkhash].js, // 根据chunk的内容的Hash值生成文件的名称,其他只还有id,hash,hash和chunkhash后面可以使用:number来取值,默认为20位,比如[name]@[chunkhash:12].js,
            // 文件发布到线上的资源的URL前缀,一般用于描述js和css位置,举个例子,打包项目时会导出一些html,那么html里边注入的script和link的地址就是通过这里配置的
            publicPath: "https://cdn.example.com/assets/", // CDN(总是 HTTPS 协议)
            publicPath: "//cdn.example.com/assets/", // CDN (协议相同)
            publicPath: "/assets/", // 相对于服务(server-relative)
            publicPath: "assets/", // 相对于 HTML 页面
            publicPath: "../assets/", // 相对于 HTML 页面
            publicPath: "", // 相对于 HTML 页面(目录相同)
            // 当需要构建的项目可以被其他模块导入使用,会用到libraryTarget和library
            library: 'xxx', // 配置导出库的名称,但是和libraryTarget有关,如果是commonjs2默认导出这个名字就没啥用
            // 从webpack3.1.0开始,可以为每个target起不同的名称
            library: {
                root: "MyLibrary",
                amd: "my-library",
                commonjs: "my-common-library"
            },
            libraryTarget: 'umd', // 导出库的类型,枚举值: umd、commonjs2、commonjs,amd、this、var(默认)、assign、window、global、jsonp
            // 需要单独导出的子模块,这样可以直接在引用的时候使用子模块,默认的时候是_entry_return_
            libraryExport: 'default', // __entry_return_.default
            libraryExport: 'MyModule', // __entry_return_.MyModule
            libraryExport: ['MyModule', 'MySubModule '], // 使用数组代表到指定模块的取值路径 __entry_return_.MyModule.MySubModule
            // 配置无入口的chunk在输出时的文件名称,但仅用于在运行过程中生成的Chunk在输出时的文件名称,这个应该一般和插件的导出有关,支持和filename一样的内置变量
            chunkFilename: '[id].js',
            // 是否包含文件依赖相关的注释信息,在mode为development默认为true
            pathinfo: true,
            // JSONP异步加载chunk,或者拼接多个初始chunk(CommonsChunkPlugin,AggressiveSplittingPlugin)
            jsonpFunction: 'myWebpackJsonp',
            // 此选项会向硬盘写入一个输出文件,只在devtool启动了sourceMap选项时采用,默认为`[file].map`,除了和filename一样外还可以使用[file]
            sourceMapFilename: '[file].map',
            // 浏览器开发者工具里显示的源码模块名称,此选项仅在 「devtool 使用了需要模块名称的选项」时使用,使用source-map调试,关联模块鼠标移动到上面的时候显示的地址,默认这个值是有的,一般不需要关心
            devtoolModuleFilenameTemplate: 'test://[resource-path]'
        },
        // 配置模块相关
        module: {
            rules: [ // 配置loaders
                {
                    test: /\.jsx?$/, // 匹配规则,匹配文件使用,一般使用正则表达值
                    include: [path.resolve(__dirname, 'app')], // 只会命中这个目录文件
                    exclude: [path.resolve(__diranme, 'app/demo-files')], // 命中的时候排除的目录
                    use: [ // 使用的loader,每一项为一个loader,从该数组的最后一个往前执行
                        'style-loader', // loader的名称,这样则是使用默认配置,可以在后面加!配置属性,也可以用下面方式
                        {
                            loader: 'css-loader', // loader的名称
                            options: {} // loader接受的参数
                        }
                    ],
                    noParse: [ // 不用解析和处理的模块 RegExp | [RegExp] | function(从 webpack 3.0.0 开始)
                        /jquery|lodash/
                    ]
                }
            ]
        },
        // 配置插件
        plugins: [
          // 压缩js的plugin
          new webpack.optimize.UglifyJsPlugin({
            compress: {
              warnings: false,
              drop_console: false,
            }
          }),
        ],
        // 解析文件引用的模块的配置
        resolve: {
            // 模块的根目录,默认从node_modules开始找
            modules: [
                'node_modules',
                'browser_modules'
            ],
            // 模块的后缀名,我们引入模块有时候不写扩展名,自动补充扩展名的顺序如下
            extensions: ['.js', '.json', '.jsx', '.css'],
            // 模块解析时候的别名
            alias: {
                // 那么导入模块时则可以写import myComponent from '$component/myComponent';
                $component: './src/component',
                // 末尾加$精确匹配
                xyz$: path.resolve(__dirname, 'path/to/file.js')
            },
            // 此选项决定优先使用package.json配置哪份导出文件
            mainFields: ['jsnext:main', 'browser', 'main'],
            // 是否强制导入语句写明后缀
            enforceExtension: false,
            // 是否将符号链接(symlink)解析到它们的符号链接位置(symlink location)
            symlinks: true,
        },
        // 选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
        devtool: 'source-map',
        // 配置输出代码的运行环境,可以为async-node,electron-main,electron-renderer,node,node-webkit,web(默认),webworker
        target: 'web',
        externals: { // 使用来自于js运行环境提供的全局变量
            jquery: 'jQuery'
        },
        // 控制台输出日志控制
        stats: {
            assets: true, // 添加资源信息
            colors: true, // 控制台日志信息是否有颜色
            errors: true, // 添加错误信息
            errorDetails: true, // 添加错误的详细信息(就像解析日志一样)
            hash: true, // 添加 compilation 的哈希值
        },
        devServer: { // 本地开发服务相关配置
            proxy: { // 代理到后端服务接口
                '/api': 'http://localhost:3000'
            },
            contentBase: path.join(__dirname, 'public'), // 配置devserver http服务器文件的根目录
            compress: true, // 是否开启gzip压缩
            hot: true, // 是否开启模块热交换功能
            https: false, // 是否开启https模式
            historyApiFallback: true, // 是否开发HTML5 History API网页
        },
        profile: true, // 是否捕捉webpack构建的性能信息,用于分析是什么原因导致的构建性能不佳
        cache: false, // 缓存生成的 webpack 模块和 chunk,来改善构建速度。缓存默认在观察模式(watch mode)启用。
        cache: {
            // 如果传递一个对象,webpack 将使用这个对象进行缓存。保持对此对象的引用,将可以在 compiler 调用之间共享同一缓存:
            cache: SharedCache  // let SharedCache = {}
        },
        watch: true, // 是否启用监听模式
        watchOptions: { // 监听模式选项
            ignored: /node_modules/, // 不监听的文件或文件夹,支持正则匹配,默认为空
            aggregateTimeout: 300, //监听到变化后,300ms再执行动作,节流,防止文件更新频率太快导致重新编译频率太快
            poll: 1000 // 检测文件是否变化,间隔时间
        },
        // 输出文件的性能检查配置
        perfomance: {
            hints: 'warning', // 有性能问题时输出警告
            hints: 'error', // 有性能问题时输出错误
            hints: false, // 关闭性能检查
            maxAssetSize: 200000, // 最大文件大小,单位bytes
            maxEntrypointSize: 400000, // 最大入口文件的大小,单位bytes
            // 此属性允许 webpack 控制用于计算性能提示的文件。
            assetFilter: function(assetFilename) {
                return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
            }
        }
    }
  3. 你常用的 loader 有哪些?

    • file-loader:加载文件资源,如字体、图片,具有移动、复制、删除功能。
    • url-loader:计算文件的 base64 编码,常用于加载较小图片,转化为 DataURL 来减少请求次数。
    • babel-loader:加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题。
    • ts-loader:加载 ts / tsx 文件,编译 TypeScript。
    • style-loader:将 css 文件以 <style> 的形式插入 html 中。
    • css-loader:分析 @import url() 引入 css 文件和对应资源。
    • less-loader / sass-loader:css 预处理器,在 css 中新增了许多语法,提高了开发效率。
    • html-minify-loader:压缩HTML。
  4. 介绍一下 webpack 常用的 plugin?

    通过配置文件导出对象中plugins属性传入new实例对象。其本质是一个具有apply方法的javascript对象。

    用于修改行为:

    • DefinePlugin:编译时配置全局变量。
    • ignore-plugin:忽略部分文件。

    用于优化:

    • extract-text-webpack-plugin:提取 CSS 到一个单独的文件中。
    • optimize-css-assets-webpack-plugin:对 css 快速去重。
    • webpack-bundle-analyzer:一个webpack的bundle文件分析工具,将bundle文件以可交互缩放的treemap的形式展示。
    • happypack:通过多进程模型加快代码构建。
    • DllPlugin:分离打包加快构建。
    • prepack-webpack-plugin:通过 Facebook 的 Prepack 优化输出的 JavaScript 代码性能。
    • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码。
    • imagemin-webpack-plugin:压缩图片文件。
    • HotModuleReplacementPlugin:模块热替换。

    用于提高开发效率:

    • ProvidePlugin:从环境中提供的全局变量中加载模块,而不用导入对应的文件,代替require 和 import。
    • html-webpack-plugin:可以根据模板自动生成 html 代码,并自动引用 css 和 js 文件。
    • web-webpack-plugin:方便的为单页应用输出 HTML,比 html-webpack-plugin 好用。
    • clean-webpack-plugin:自动清理构建目录。
  5. 说说 Loader 和 Plugin 的区别?

    从功能上来看:

    loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

    plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他功能。


    从整个运行时机上来看:

    loader 运行在打包之前。

    plugins 在整个编译周期都起作用。


    从原理上看:

    loader 实质是一个转换器函数。因为 webpack 运行在 nodeJS 环境,只能识别 js 文件,loader 以 webpack 对象作为自己的上下文,接收一个文件源内容作为参数,对其进行编译转化。

    plugins 本质是发布订阅模式中的订阅者。它监听了 webpack 生命周期的一些事件,在事件广播时触发回调函数,执行其中的逻辑,通过 webpack 的 API 来改变 webpack 行为。在配置 plugins 时需要 new 一个实例对象。

  6. 说说 webpack 的热更新是如何做到的?原理是什么?

    HMR 全称 Hot Module Replacement,可以理解为模块热替换,指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用。

    例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失。如果使用的是 HMR,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用。

    webpack中配置开启:

    const webpack = require('webpack')
    module.exports = {
      // ...
      devServer: {
        // 开启 HMR 特性
        hot: true
        // hotOnly: true
      }
    }

    需要有一些额外的操作,指定哪些模块发生更新时进行 HMR

    if(module.hot){
        module.hot.accept('./util.js',()=>{
            console.log("util.js更新了")
        })
    }

    原理:

    通过 webpack-dev-server 建立两个服务器:静态资源代理服务器(express)和 socket 服务器。

    express server 将打包后的资源发送给浏览器,通过 AJAX 被浏览器请求和解析。

    socket server 建立 websocket 长连接,实现双向瞬时通信。

    socket server 监听到对应的模块变动,生成两个文件 .json(manifest 文件,包含了 hashchundId,用来说明变化的内容) 和 .js(update chunk)。

    通过长连接,socket server 可以直接将这两个文件主动推送给浏览器。

    浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新。

  7. 说说 webpack proxy 工作原理?为什么能解决跨域?

    webpack proxy,即webpack提供的代理服务,基本行为就是接收客户端发送的请求后转发给其他服务器。

    其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)。

    想要实现代理首先需要一个中间服务器,webpack中提供服务器的工具为webpack-dev-server只适用在开发阶段

    proxy的工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器,利用服务器之间通信不存在跨域来解决问题。目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地。

  8. tree-shaking 如何配置?工作原理?

    Tree-shaking,即消除那些被引用了但未被使用的模块代码。

    想要对代码进行 tree-shaking,首先必须处于生产模式。Webpack 只有在压缩代码的时候会 tree-shaking,而这只会发生在生产模式中。

    其次,需要采用 ES6 模块化语法,tree-shaking 是静态编译时优化,使用 commonJS 的模块无法被分析。

    随着 Webpack4.0 采用约定高于配置的理念,在 mode 为 production 的情况默认开启优化配置项。

    默认使用的是 terser-webpack-plugin 压缩插件,在此之前是使用 uglifyjs-webpack-plugin,其中的区别是内置对 ES6 的压缩不是很好。

    注意默认情况下,所有的模块都会被认为是有副作用的,Tree Shaking 不能删除带副作用的代码。所以,webpack 提供了 sideEffects 这个配置,在 package.json 中将这个配置设置为 false,意思就是 webpack 认为所有文件都是没有副作用的。


    原理:

    Tree Shaking 会分析所有的 es6 module import 和 export,判断哪些是无用的代码,同时为它们打上标记,然后使用压缩工具(TerserPlugin)删除这些冗余的代码。

  9. 说说 webpack 的代码分割?解决了哪些问题?

    code-spliting 是提高代码使用率的关键技术,即代码分割。代码分割是指,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程。在 Webpack 构建时,会避免加载已声明要异步加载的代码 ,异步代码会被单独分离出一个文件,当代码实际调用时被加载至页面。

    可以通过 import() 关键字让浏览器在程序执行时异步加载相关资源。

    代码分割也包括静态和动态分割两种。

    静态代码分割是指,在代码中明确声明需要异步加载的代码

    import Listener from './listeners.js'
    const getModal = () => import('./src/modal.js') 
    Listener.on(
      'didSomethingToWarrentModalBeingLoaded',
      () => {
        // Async fetching modal code from a separate chunk
        getModal().then(
          (module) => {
            const modalTarget = document.getElementById('Modal')    
            module.initModal(modalTarget)  
          })
      }
    )

    动态代码分割是指:在代码调用时根据当前的状态,「动态地」异步加载对应的代码块

    const getTheme = (themeName) => import(`./src/themes/${themeName}`)
    // using `import()` 'dynamically'
    if (window.feeling.stylish) {
      getTheme('stylish').then((module) => {
        module.applyTheme()
      })
    } else if (window.feeling.trendy) {
      getTheme('trendy').then((module) => {
        module.applyTheme()
      })
    }

    实现了类似 commonJS 的效果。但它们的工作原理却大不相同。实际上 webpack 已经在构建时将所有声明的分割模块分离为一个单独的文件(contextModule),在调用时和静态分割一样,是在请求一个已经准备好的文件。

  10. 什么是 webpack 的作用域提升?为什么要作用域提升?

    可以简单地把 Scope Hoisting 理解为把每个模块被 Webpack 处理成模块初始化函数整理到一个统一的包裹函数里,也就是把多个作用域用一个作用域取代,以减少内存消耗并减少包裹块代码,从每个模块有一个包裹函数变成只有一个包裹函数包裹所有的模块。

    但是有一个前提就是,当模块的引用次数大于 1 时,比如被引用了两次或以上,那么这个效果会无效,这是因为被引用多次即这个模块代码会被内联多次,从而增加了打包出来的 JS Bundle 体积。也就是被引用多次的模块在被 Webpack 处理后,会被独立的包裹函数所包裹。


    需要作用域提升的原因:

    • 大量函数闭包包裹代码,导致体积增大,模块越多越明显
    • 运行代码时创建的函数作用域变多,内存开销变大
  11. SourceMap 有什么用?

    Source map 就是一个信息文件,里面储存着代码的位置信息。这种文件主要用于开发调试,现在代码都会经过压缩混淆,这样报错提示会很难定位代码。通过 SourceMap 能快速定位到源代码,并进行调试。

    通常情况 SourceMap 在开发环境开启,线上环境关闭。

    开发环境推荐:

    cheap-module-eval-source-map

    生产环境推荐:

    cheap-module-source-map 、hidden-source-map

    应用场景:

    • 开发期间,开发人员能直接通过浏览器调试工具直接定位错误或进行 Debug 调试。
    • 线上排查问题的时候可以将 SourceMap 上传到错误监控系统。
  12. 如何提高webpack的构建速度?

    常见的提升构建速度的手段有如下:

    • 优化 loader 配置

      在使用loader时,可以通过配置includeexcludetest属性来匹配文件,includeexclude 规定哪些匹配应用loader

    • 合理使用 resolve.extensions

      通过resolve.extensions 在解析文件时自动添加拓展名,当我们引入文件的时候,若没有文件后缀名,则会根据数组内的值依次查找,配置的时候,不要随便把所有后缀都写在里面,这会调用多次文件的查找,这样就会减慢打包速度。

    • 优化 resolve.modules

      resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块。所以可以指明存放第三方模块的绝对路径,以减少寻找。

    • 优化 resolve.alias

      alias给一些常用的路径起一个别名,当我们的项目目录结构比较深的时候,一个文件的路径可能是./../../的形式,通过配置alias以减少查找过程。

    • 使用 cache-loader

      在一些性能开销较大的 loader之前添加 cache-loader,以将结果缓存到磁盘里,显著提升二次构建速度,保存和读取这些缓存文件会有一些时间开销,所以只对性能开销较大的 loader 使用。

    • 启动多线程

      借助一些插件如 HappyPack,使用多进程并行运行来提高构建速度。

    • 合理使用 sourceMap

      打包生成 sourceMap 的时候,如果信息越详细,打包速度就会越慢。因此根据实际需要选择最合适的配置。

Redux

  1. 说说你对Redux的理解?其工作原理?

    在 React 应用中存在很多个组件,每个组件的state是由自身进行管理,包括组件定义自身的state、组件之间的通信通过props传递、使用Context实现数据共享。

    如果让每个组件都存储自身相关的状态,理论上来讲不会影响应用的运行,但在开发及后续维护阶段,我们将花费大量精力去查询状态的变化过程。这种情况下,如果将所有的状态进行集中管理,当需要更新状态的时候,仅需要对这个管理集中处理,而不用去关心状态是如何分发到每一个组件内部的。

    redux就是一个实现上述集中管理的容器,遵循三大基本原则:

    • 单向数据流
    • 不可变数据
    • 纯函数

    工作原理:

    通过在 context 注入一个全局的 store,集中式地管理数据。

    为 store 指定 reducer,完成连接。

    其内部的 state 对象一旦创建就不可变,更新 state,采用全新的 state 替换原本的 state。

    通过 Action Creator 定义不同的 Action,来操作不同的 state。

    通过 reducer 的 dispatch 操作,传入 Action 来修改组件内部状态。

  2. 你在React项目中是如何使用Redux的? 项目结构是如何划分的?

    通过redux将整个应用状态存储到store中,组件可以派发dispatch行为actionstore

    其他组件通过订阅store中的状态state来更新自身的视图。

    可以根据项目具体情况进行选择,以下列出两种常见的组织结构:

    • 按角色组织(MVC)

      按照代码在项目中的角色定位来组织,如:

      reducers/
        todoReducer.js
        filterReducer.js
      actions/
        todoAction.js
        filterActions.js
      components/
        todoList.js
        todoItem.js
        filter.js
      containers/
        todoListContainer.js
        todoItemContainer.js
        filterContainer.js
    • 按功能组织

      把完成同一功能的代码放在一个目录下,一个应用功能包含多个角色的代码。如:

      todoList/
        actions.js
        actionTypes.js
        index.js
        reducer.js
        views/
          components.js
          containers.js
      filter/
        actions.js
        actionTypes.js
        index.js
        reducer.js
        views/
          components.js
          container.js
  3. 说说对Redux中间件的理解?常用的中间件有哪些?实现原理?

    中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务,衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。

    Redux 中如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件,中间件就是放在就是在 dispatch 过程,在分发 action 时进行拦截处理。

    本质上是一个函数,在内部对 store.dispatch 进行了封装,在发出 action 和执行 reducer 之间,添加了一些额外的操作。

    常用的中间件有:

    • redux-thunk:用于异步操作
    • redux-logger:用于日志记录

    上述的中间件都需要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,依次执行

    然后作为第二个参数传入到createStore中。

    const store = createStore(
      reducer,
      applyMiddleware(thunk, logger)
    );

    实现原理:

    看看applyMiddlewares的源码:

    export default function applyMiddleware(...middlewares) {
      return (createStore) => (reducer, preloadedState, enhancer) => {
        var store = createStore(reducer, preloadedState, enhancer);
        var dispatch = store.dispatch;
        var chain = [];
    
        var middlewareAPI = {
          getState: store.getState,
          dispatch: (action) => dispatch(action)
        };
        chain = middlewares.map(middleware => middleware(middlewareAPI));
        dispatch = compose(...chain)(store.dispatch);
    
        return {...store, dispatch}
      }
    }

    所有中间件被放进了一个数组chain,然后遍历执行,最后将所有的中间件组合成一个函数,传入 store.dispatch

  4. 了解过 Redux-thunk 吗?能不能手动实现?

    function createThunkMiddleware(extraArgument) {
      return ({ dispatch, getState }) => (next) => (action) => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    
    export default thunk;

    从源码来看,redux-thunk 非常简单。如果传入的 action 是函数,就直接执行它,并传入 dispatch 等作为参数。在 action 函数内部,就可以在异步代码中执行 dispatch 了。

项目性能优化

  1. 说说如何借助webpack来优化前端性能?

    通过webpack优化前端的手段有:

    • JS 代码压缩

      比较常用的 uglifyjs-webpack-plugin、TerserPlugin 都是对 JS 文件进行压缩处理。

    • CSS 代码压缩

      css-minimizer-webpack-plugin,通常是去除无用的空格。optimize-css-assets-webpack-plugin 可以对 css 进行去重。

    • HTML 代码压缩

      使用 HtmlWebpackPlugin 插件来生成 HTML 模板时,通过配置属性 minify 进行 html 优化。

    • 文件大小压缩

      ComepressionPlugin,减少文件体积,减少 http 传输过程中的损耗。

    • 图片大小压缩

      在打包之后,一些图片文件的大小是远远要比 js 或者 css 文件要大,所以图片压缩较为重要。

      可在 image-webpack-loader 中配置不同格式的图片压缩,或者 url-loader 将较小的图片转换为 base64 编码。

    • Tree Shaking

      实现 Tree shaking 有两种不同的方案:

      usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化

      sideEffects:webpack 将认为整个文件无副作用,完全移除所有未使用的代码。

    • 代码分割

      默认情况下,所有的 JavaScript 代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度。

      将代码分离到不同的 bundle 中,之后我们可以按需加载,或者并行加载这些文件。

      代码分离可以分出更小的 bundle,以及控制资源加载优先级,提供代码的加载性能。

    • 作用域提升

      把一些包裹模块的独立函数整合到一个统一的包裹函数中,以减少内存消耗并减少包裹块代码。

  2. 谈谈你对编码优化的理解?

    编码优化,指的就是 在代码编写时,通过一些最佳实践,提升代码的执行性能。通常这并不会带来非常大的收益,但这属于程序猿的自我修养。


    循环: 通常是编码性能的关键点,代码的性能问题会再循环中被指数倍放大。

    • 尽可能 减少循环次数,包括减少遍历的数据量,完成目的后立即终止循环
    • 避免在循环中执行大量的运算,避免重复计算,相同的执行结果应该使用缓存
    • 尽量避免使用 for-in 循环,因为它会枚举原型对象,耗时大于普通循环。

    条件流程性能: Map / Object > switch > if-else


    数据读取:

    • 尽量在局部作用域中进行变量缓存。
    • 避免嵌套过深的数据结构,数据扁平化 有利于数据的读取和维护。

    dom 优化:

    • 减少访问 dom 的次数,如需多次,将 dom 缓存,如 useRef。
    • 减少重绘与回流,多次操作合并为一次,减少对计算属性的访问。大量操作时,可将 dom 脱离文档流或者隐藏,待操作完成后再重新恢复。
    • 使用事件委托,避免大量的事件绑定。

    css 优化:

    • 层级扁平,避免过于多层级的选择器嵌套。
    • 特定的选择器 好过一层一层查找: .xxx-child-text{} 优于 .xxx .child .text{}
    • 减少使用通配符与属性选择器
    • 减少不必要的多余属性
    • 使用 动画属性 实现动画,动画时脱离文档流,开启硬件加速,优先使用 css 动画。
    • 使用 <link> 替代原生 @import

    html 优化:

    • 减少 dom 数量,避免不必要的节点或嵌套。
    • 避免<img src="" />空标签,能减少服务器压力,因为 src 为空时,浏览器仍然会发起请求。
    • 图片提前 指定宽高 或者 脱离文档流,能有效减少因图片加载导致的页面回流。
    • 语义化标签 有利于 SEO 与浏览器的解析时间。
    • 减少使用 table 进行布局,避免使用<br/><hr/>
  3. 在页面的基础上可以做哪些优化?

    引入位置

    • css 文件<head>中引入, js 文件<body>底部引入。
    • 影响首屏的,优先级很高的 js 也可以头部引入,甚至内联。

    减少请求(http 1.0 - 1.1):

    • 合并请求,正确设置 http 缓存。

    减少文件体积:

    • 删除多余代码,tree-shaking,UglifyJs 等。
    • 混淆 / 压缩代码,开启 gzip 压缩。
    • 多份编译文件按条件引入,针对现代浏览器直接给 ES6 文件,只针对低端浏览器引用编译后的 ES5 文件。
    • 动态 polyfill,只针对不支持的浏览器引入 polyfill。

    图片优化:

    • 根据业务场景,与 UI 探讨选择合适质量,合适尺寸
    • 根据需求和平台,选择合适格式,例如非透明时可用 jpg;非 IOS 端,使用 webp。
    • 小图片合成雪碧图,低于 5K 的图片可以转换成 base64 内嵌。
    • 合适场景下,使用 iconfont 或者 svg

    使用缓存:

    • 浏览器缓存,通过设置请求的过期时间,合理运用浏览器缓存。
    • CDN缓存,静态文件合理使用 CDN 缓存技术。
    • 服务器缓存,将不变的数据、页面缓存到内存或远程存储(redis等)上。
    • 数据缓存: 通过各种存储将不常变的数据进行缓存,缩短数据的获取时间。
  4. 首屏渲染优化有哪些方式?

    加载优化:

    • 代码分割,使首屏依赖的文件体积最小。
    • 非关键性的文件尽可能的异步加载和懒加载,避免阻塞首屏渲染。
    • 使用dns-prefetch / preconnect / prefetch / preload等浏览器提供的资源提示,加快文件传输。
    • 谨慎控制好 Web字体,控制字体包的加载时机,如果使用的字体有限,那尽可能只将使用的文字单独打包,能有效减少体积。
    • 合理利用 Localstorage / server-worker 等存储方式进行 数据与资源缓存

    顺序优化:

    • 分清轻重缓急,重要的元素优先渲染,视窗内的元素优先渲染。

    用户感知优化:

    • 利用一些动画过渡效果,能有效减少用户对卡顿的感知
    • 尽可能利用骨架屏(Placeholder) / Loading 等减少用户对白屏的感知。
    • 动画帧数尽量保证在30帧以上,低帧数、卡顿的动画宁愿不要。
    • js 执行时间避免超过 100ms,寻找可缓存的点,任务分割异步或 web worker 执行。

    服务端渲染(SSR):

    • 减少首屏需要的数据量,剔除冗余数据和请求。
    • 控制好缓存,对数据/页面进行合理的缓存。
    • 页面的请求使用流的形式进行传递。
  5. 知道哪些性能优化的指标?

    资源加载:

    chrome 瀑布图,打开 NetWork,

后记

待续。