引言

在过去的几年里,JavaScript 框架已经完全改变了我们构建应用程序的方式,React 在转换过程中也占有一席之地。 优化页面加载时间非常重要,因为页面直接加载所需的时间与跳出率和转化率有关。 在本教程中,我们将看看大多数开发人员在使用 React 构建应用程序时常犯的六个错误。 我们还将讨论如何避免这些错误,并重点介绍一些有用的技巧,以尽可能减少页面加载时间。

反应如何起作用

上面的图片解释了你每次使用 React 应用时会发生什么。 每个 React 应用程序都以一个根组件开始(在我们的例子中是 App) ,由“ children”组件(addgrocerylist 和 SearchBar)组成。 这些子组件由基于其中包含的属性和状态将 UI 呈现给 DOM 的函数组成。

用户以不同的方式与呈现的用户界面进行交互ーー填写表单或单击按钮。 当这种情况发生时,事件被中继回到父组件。 这些事件会导致应用程序状态的变化,从而提示 React 在其虚拟 DOM 中重新呈现 UI。

对于每个发送回来的事件,React 的引擎必须将虚拟 DOM 与真实 DOM 进行比较,并计算是否需要用这些新发现的数据更新真实 DOM。 现在,事情变得混乱的地方是,如果我们的应用程序的结构方式,每一次点击和滚动的结果反复比较和更新虚拟和真实 DOM 之间的变化。 这可能会导致极其缓慢和低效的应用程序。

常见错误及其避免

下面是开发人员在构建应用程序时做出的三个糟糕的实践。 这些错误会降低效率,增加页面加载时间,尽可能地避免它们。

不必要的进口

无论何时在应用程序中导入整个库,都会对其进行析构以访问所需的模块。 一个或两个小库可能不会造成损害,但是当你导入大量的库和依赖项时,你应该只导入你需要的模块:

import assign from "101";
import Map from "immutable";

上面的代码导入整个库,并开始对其进行解构以访问 assign 和 Map。 与其这样做,不如使用一个叫做“精挑细选”的概念,它只抓取应用程序需要的必要部分:

import assign from "101/assign";
import Map from "immutable/src/map";

Jsx 中的嵌入函数

众所周知,JavaScript 是一种垃圾回收语言。 它的垃圾收集器通过尝试回收应用程序的某些部分不再使用的内存来管理内存。 在 render 中定义一个函数将在每次重新呈现包含组件时创建一个新的函数实例。 当这种情况出现在你的应用程序中时,它最终会以内存泄漏的形式成为垃圾收集器的一个问题ー即应用程序的某些部分使用的内存没有被释放,即使这些部分已经不再使用:

class GroceryList extends React.Component {
    state = {
        groceries: [],
        selectedGroceryId: null
    }

    render(){
        const { groceries } = this.state;
        return (
           groceries.map((grocery)=>{
               return <Grocery onClick={(e)=>{
                    this.setState({selectedGroceryId:grocery.groceryId})
               }} grocery={grocery} key={grocery.id}/>
           }) 
        )
    }
}

正确的做法是在渲染之前定义一个新的函数 ongloceryclick:

class GroceryList extends React.Component {
    state = {
        groceries: [],
        selectedGroceryId: null
    }

    onGroceryClick = (groceryId)=>{
        this.setState({selectedGroceryId:groceryId})
    }

    render(){
        const { groceries } = this.state;
        return (
           groceries.map((grocery)=>{
               return <Grocery onClick={this.onGroceryClick} 
                grocery={grocery} key={grocery.id}/>
           }) 
        )
    }
}

在 DOM 元素中使用扩展运算符

在构建应用程序时,以一种无保留的方式使用扩展操作符是一种糟糕的做法。 这将有未知的属性飞来飞去,事情开始变得复杂只是一个时间问题。 下面是一个简短的例子:

const GroceriesLabel = props => {
    return (
      <div {...props}>
        {props.text}
      </div>
    );
  };

随着你构建的应用程序变得越来越大,... 道具可以包含任何东西。 更好、更安全的做法是确定你需要的价值观:

const GroceriesLabel = props => {
    return (
      <div particularValue={props.particularValue}>
        {props.text}
      </div>
    );
};

性能技巧

使用 Gzip 压缩你的包

这是非常有效的,可以减少多达65% 的文件大小。 应用程序中的大多数文件都会使用大量重复的文本和空白。 Gzip 通过压缩这些反复出现的字符串来处理这个问题,从而大大缩短了网站的第一次渲染时间。 Gzip 使用 compressionPlugin 对文件进行预压缩ー Webpack 的本地插件用于在生产过程中压缩文件,首先让我们使用 compressionPlugin 创建一个压缩包:

plugins: [
    new CompressionPlugin({
        asset: "[path].gz[query]",
        algorithm: "gzip",
        test: /.js$|.css$|.html$/,
        threshold: 10240,
        minRatio: 0.8
    })
]

一旦文件被压缩,就可以使用中间件为其提供服务:

//this middleware serves all js files as gzip
app.use(function(req, res, next) {
    const originalPath = req.path;
    if (!originalPath.endsWith(".js")) {
        next();
        return;
    }
    try {
        const stats = fs.statSync(path.join("public", `${req.path}.gz`));
        res.append('Content-Encoding', 'gzip');
        res.setHeader('Vary', 'Accept-Encoding');
        res.setHeader('Cache-Control', 'public, max-age=512000');
        req.url = `${req.url}.gz`;

        const type = mime.lookup(path.join("public", originalPath));
        if (typeof type != 'undefined') {
            const charset = mime.charsets.lookup(type);
            res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
        }
    } catch (e) {}
    next();
})

制表反应组件

制表的概念并不新鲜---- 它是一种存储昂贵的函数调用并在同一输入再次出现时返回已缓存结果的技术。 React 中的 Memoization 通过制表数据计算来工作,以便状态变化尽可能快地发生。 要理解这是如何工作的,看看下面的 React 组件:

const GroceryDetails = ({grocery, onEdit}) => {
    const {name, price, grocery_img} = grocery;
    return (
        <div className="grocery-detail-wrapper">
            <img src={grocery_img} />
            <h3>{name}</h3>
            <p>{price}</p>
        </div>
    )
}

在上面的代码示例中,GroceryDetails 中的所有子元素都基于道具。 更换道具会导致 GroceryDetails 重新渲染。 如果 GroceryDetails 是一个不太可能改变的组件,它应该被制表。 React (V16.6.0)早期版本的用户将使用 moize,一个用于 JavaScript 的制表库。 看看下面的语法:

import moize from 'moize/flow-typed';

const GroceryDetails = ({grocery, onEdit}) =>{
    const {name, price, grocery_img} = grocery;

    return (
        <div className="grocery-detail-wrapper">
            <img src={grocery_img} />
            <h3>{name}</h3>
            <p>{price}</p>
        </div>
    )
}

export default moize(GroceryDetails,{
    isReact: true
});

在上面的代码块中,moize 存储 GroceryDetails 中任何传递的道具和上下文,并使用它们来检测组件是否有更新。 只要道具和上下文保持不变,就会返回缓存的值。 这确保了超快速的渲染。

对于使用 React 更新版本(大于 V16.6.0)的用户,可以使用 React.memo 而不是 moize:

const GroceryDetails = ({grocery, onEdit}) =>{
    const {name, price, grocery_img} = grocery;

    return (
        <div className="grocery-detail-wrapper">
            <img src={grocery_img} />
            <h3>{name}</h3>
            <p>{price}</p>
        </div>
    )
}

export default React.memo(GroceryDetails);

考虑使用服务器端渲染

服务器端呈现(Server-Side Rendering,SSR)是前端框架在后端系统上运行时呈现标记的能力。

您应该利用单页应用程序的 SSR。 与其让用户等待 JavaScript 文件加载,不如让应用程序的用户在初始请求返回响应时立即收到一个完全渲染的 HTML 页面。 通常,服务器端呈现的应用程序使用户能够比客户端呈现的应用程序更快地接收内容。 React 中服务器端渲染的一些解决方案包括 Next.js 和 Gatsby。

总结

对于一般不熟悉这些概念的初学者来说,我建议你参考一下这个课程,开始学习 React,因为它涵盖了从处理状态到创建组件的大量内容。 在 React 应用程序中优化页面加载时间的重要性不可低估。 你的应用程序不仅可以帮助更好的搜索引擎优化和获得更高的转化率,通过遵循最佳实践,你将能够更容易地检测错误。 这些概念可能需要很多努力,但是值得一试。 像 Tony Quetano 的 Memoize React Components 和 Selva Ganesh 的 How to serve a Webpack gzipped file in production 这样的文章帮助我们编写了这篇文章。