Webpack笔记七

86 阅读13分钟

性能优化概述

本章所讲的性能优化主要体现在三个方面:

2020-02-12-09-53-01.png

构建性能

这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能

优化的目标,是降低从打包开始,到代码效果呈现所经过的时间

构建性能会影响开发效率。构建性能越高,开发过程中时间的浪费越少

传输性能

传输性能是指,打包后的JS代码传输到浏览器经过的时间

在优化传输性能时要考虑到:

  1. 总传输量:所有需要传输的JS文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
  2. 文件数量:当访问页面时,需要传输的JS文件数量,文件数量越多,http请求越多,响应速度越慢
  3. 浏览器缓存:JS文件会被浏览器缓存,被缓存的文件不会再进行传输

运行性能

运行性能是指,JS代码在浏览器端的运行速度

它主要取决于我们如何书写高性能的代码

永远不要过早的关注于性能,因为你在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率


性能优化主要从上面三个维度入手

性能优化没有完美的解决方案,需要具体情况具体分析

-------------------华丽分割线---------------------------

减少模块解析

什么叫做模块解析?

2020-02-13-16-26-41.png

模块解析包括:抽象语法树分析、依赖分析、模块语法替换

不做模块解析会怎样?

2020-02-13-16-28-10.png

如果某个模块不做解析,该模块经过loader处理后的代码就是最终代码。

如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。

如果不对某个模块进行解析,可以缩短构建时间

哪些模块不需要解析?

模块中无其他依赖:一些已经打包好的第三方库,比如jquery

如何让某个模块不要解析?

配置module.noParse,它是一个正则,被正则匹配到的模块不会解析

module.exports = {
    mode: "development",
    devtool: "source-map",
    module: {
        noParse: /test/
    }
}

-------------------华丽分割线---------------------------

优化loader性能 {ignore}

进一步限制loader的应用范围

思路是:对于某些库,不使用loader

例如:babel-loader可以转换ES6或更高版本的语法,可是有些库本身就是用ES5语法书写的,不需要转换,使用babel-loader反而会浪费构建时间

lodash就是这样的一个库

lodash是在ES5之前出现的库,使用的是ES3语法

通过module.rule.excludemodule.rule.include,排除或仅包含需要应用loader的场景

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /lodash/,
                use: "babel-loader"
            }
        ]
    }
}

如果暴力一点,甚至可以排除掉node_modules目录中的模块,或仅转换src目录的模块

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                //或
                // include: /src/,
                use: "babel-loader"
            }
        ]
    }
}

这种做法是对loader的范围进行进一步的限制,和noParse不冲突,想想看,为什么不冲突

缓存loader的结果

我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变

于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果

cache-loader可以实现这样的功能

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['cache-loader', ...loaders]
      },
    ],
  },
};

有趣的是,cache-loader放到最前面,却能够决定后续的loader是否运行

实际上,loader的运行过程中,还包含一个过程,即pitch

2020-02-21-13-32-36.png

cache-loader还可以实现各自自定义的配置,具体方式见文档

为loader的运行开启多线程

thread-loader会开启一个线程池,线程池中包含适量的线程

它会把后续的loader放到线程池的线程中运行,以提高构建效率

由于后续的loader会放到新的线程中,所以,后续的loader不能:

  • 使用 webpack api 生成文件
  • 无法使用自定义的 plugin api
  • 无法访问 webpack options

在实际的开发中,可以进行测试,来决定thread-loader放到什么位置

特别注意,开启和管理线程需要消耗时间,在小型项目中使用thread-loader反而会增加构建时间

module.exports = {
  mode: "development",
  devtool: "source-map",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          // {
          //   loader: "cache-loader",
          //   options:{
          //     cacheDirectory: "./cache"
          //   }
          // },
          "thread-loader",
          "babel-loader"
        ]
      }
    ]
  }
};

-------------------华丽分割线---------------------------

热替换 HMR

热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间

当使用webpack-dev-server时,考虑代码改动到效果呈现的过程

2020-02-21-14-20-49.png

而使用了热替换后,流程发生了变化

2020-02-21-14-22-32.png

使用和原理

  1. 更改配置
module.exports = {
  devServer:{
    hot:true // 开启HMR
  },
  plugins:[ 
    // 可选
    new webpack.HotModuleReplacementPlugin()
  ]
}
  1. 更改代码
// index.js

if(module.hot){ // 是否开启了热更新
  module.hot.accept() // 接受热更新
}

首先,这段代码会参与最终运行!

当开启了热更新后,webpack-dev-server会向打包结果中注入module.hot属性

默认情况下,webpack-dev-server不管是否开启了热更新,当重新打包后,都会调用location.reload刷新页面

但如果运行了module.hot.accept(),将改变这一行为

module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器

2020-02-21-14-34-05.png

然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行

插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行

所以,热替换发生在代码运行期

样式热替换

对于样式也是可以使用热替换的,但需要使用style-loader

因为热替换发生时,HotModuleReplacementPlugin只会简单的重新运行模块代码

因此style-loader的代码一运行,就会重新设置style元素中的样式

mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的

-------------------华丽分割线---------------------------

手动分包 {ignore}

基本原理

手动分包的总体思路是:

  1. 先单独的打包公共模块

2020-02-24-13-24-57.png

公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单

  1. 根据入口模块进行正常打包

打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构

//源码,入口文件index.js
import $ from "jquery"
import _ from "lodash"
_.isArray($(".red"));

由于资源清单中包含jquerylodash两个模块,因此打包结果的大致格式是:

(function(modules){
  //...
})({
  // index.js文件的打包结果并没有变化
  "./src/index.js":
  function(module, exports, __webpack_require__){
    var $ = __webpack_require__("./node_modules/jquery/index.js")
    var _ = __webpack_require__("./node_modules/lodash/index.js")
    _.isArray($(".red"));
  },
  // 由于资源清单中存在,jquery的代码并不会出现在这里
  "./node_modules/jquery/index.js":
  function(module, exports, __webpack_require__){
    module.exports = jquery;
  },
  // 由于资源清单中存在,lodash的代码并不会出现在这里
  "./node_modules/lodash/index.js":
  function(module, exports, __webpack_require__){
    module.exports = lodash;
  }
})

打包公共模块

打包公共模块是一个独立的打包过程

  1. 单独打包公共模块,暴露变量名
// webpack.dll.config.js
module.exports = {
  mode: "production",
  entry: {
    jquery: ["jquery"],
    lodash: ["lodash"]
  },
  output: {
    filename: "dll/[name].js",
    library: "[name]"
  }
};

  1. 利用DllPlugin生成资源清单
// webpack.dll.config.js
module.exports = {
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置
      name: "[name]"//资源清单中,暴露的变量名
    })
  ]
};

运行后,即可完成公共模块打包

使用公共模块

  1. 在页面中手动引入公共模块
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>
  1. 重新设置clean-webpack-plugin

如果使用了插件clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置

new CleanWebpackPlugin({
  // 要清除的文件或目录
  // 排除掉dll目录本身和它里面的文件
  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
})

目录和文件的匹配规则使用的是globbing patterns

  1. 使用DllReferencePlugin控制打包结果
module.exports = {
  plugins:[
    new webpack.DllReferencePlugin({
      manifest: require("./dll/jquery.manifest.json")
    }),
    new webpack.DllReferencePlugin({
      manifest: require("./dll/lodash.manifest.json")
    })
  ]
}

总结

手动打包的过程

  1. 开启output.library暴露公共模块
  2. DllPlugin创建资源清单
  3. DllReferencePlugin使用资源清单

手动打包的注意事项

  1. 资源清单不参与运行,可以不放到打包目录中
  2. 记得手动引入公共JS,以及避免被删除
  3. 不要对小型的公共JS库使用

优点

  1. 极大提升自身模块的打包速度
  2. 极大的缩小了自身文件体积
  3. 有利于浏览器缓存第三方库的公共代码

缺点

  1. 使用非常繁琐
  2. 如果第三方库中包含重复代码,则效果不太理想
webconfig.js

const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const webpack = require("webpack");

module.exports = {
  mode: "development",
  devtool: "source-map",
  entry: {
    main: "./src/index.js",
    other: "./src/other.js"
  },
  output: {
    filename: "[name].[hash:5].js"
  },
  plugins: [
    new CleanWebpackPlugin({
      // 要清除的文件或目录
      // 排除掉dll目录本身和它里面的文件
      cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"]
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html"
    }),
    new webpack.DllReferencePlugin({
      manifest: require("./dll/jquery.manifest.json")
    }),
    new webpack.DllReferencePlugin({
      manifest: require("./dll/lodash.manifest.json")
    })
  ]
};

webpack.dll.config.js

const webpack = require("webpack");
const path = require("path");

module.exports = {
  mode: "production",
  entry: {
    jquery: ["jquery"],
    lodash: ["lodash"]
  },
  output: {
    filename: "dll/[name].js",
    library: "[name]" // 每个bundle暴露的全局变量名
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve(__dirname, "dll", "[name].manifest.json"),
      name: "[name]"
    })
  ]
};

-------------------华丽分割线---------------------------

自动分包 {ignore}

基本原理

不同与手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制

因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要

要控制自动分包,关键是要配置一个合理的分包策略

有了分包策略之后,不需要额外安装任何插件,webpack会自动的按照策略进行分包

实际上,webpack在内部是使用SplitChunksPlugin进行分包的 过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善,到了webpack4之后,已被SplitChunksPlugin取代

2020-02-24-17-19-47.png

从分包流程中至少可以看出以下几点:

  • 分包策略至关重要,它决定了如何分包
  • 分包时,webpack开启了一个新的chunk,对分离的模块进行打包
  • 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新chunk的产物

分包策略的基本配置

webpack提供了optimization配置项,用于配置一些优化信息

其中splitChunks是分包策略的配置

module.exports = {
  optimization: {
    splitChunks: {
      // 分包策略
    }
  }
}

事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景

  1. chunks

该配置项用于配置需要应用分包策略的chunk

我们知道,分包是从已有的chunk中分离出新的chunk,那么哪些chunk需要分离呢

chunks有三个取值,分别是:

  • all: 对于所有的chunk都要应用分包策略
  • async:【默认】仅针对异步chunk应用分包策略
  • initial:仅针对普通chunk应用分包策略

所以,你只需要配置chunksall即可

  1. maxSize

该配置可以控制包的最大字节数

如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包

但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积

另外,该配置看上去很美妙,实际意义其实不大

因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存

虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化

如果要进一步减少公共模块的体积,只能是压缩和tree shaking

分包策略的其他配置

如果不想使用其他配置的默认值,可以手动进行配置:

  • automaticNameDelimiter:新chunk名称的分隔符,默认值~
  • minChunks:一个模块被多少个chunk使用时,才会进行分包,默认值1
  • minSize:当分包达到多少字节后才允许被真正的拆分,默认值30000

缓存组

之前配置的分包策略是全局的

而实际上,分包策略是基于缓存组的

每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包

默认情况下,webpack提供了两个缓存组:

module.exports = {
  optimization:{
    splitChunks: {
      //全局配置
      cacheGroups: {
        // 属性名是缓存组名称,会影响到分包的chunk名
        // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
        vendors: { 
          test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
          priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
        },
        default: {
          minChunks: 2,  // 覆盖全局配置,将最小chunk引用数改为2
          priority: -20, // 优先级
          reuseExistingChunk: true // 重用已经被分离出去的chunk
        }
      }
    }
  }
}

很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了

但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        styles: {
          test: /\.css$/, // 匹配样式模块
          minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
          minChunks: 2 // 覆盖默认的最小chunk引用数
        }
      }
    }
  },
  module: {
    rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
      chunks: ["index"]
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // chunkFilename是配置来自于分割chunk的文件名
      chunkFilename: "common.[hash:5].css" 
    })
  ]
}

配合多页应用

虽然现在单页应用是主流,但免不了还是会遇到多页应用

由于在多页应用中需要为每个html页面指定需要的chunk,这就造成了问题

new HtmlWebpackPlugin({
  template: "./public/index.html",
  chunks: ["index~other", "vendors~index~other", "index"]
})

我们必须手动的指定被分离出去的chunk名称,这不是一种好办法

幸好html-webpack-plugin的新版本中解决了这一问题

npm i -D html-webpack-plugin@next

做出以下配置即可:

new HtmlWebpackPlugin({
  template: "./public/index.html",
  chunks: ["index"]
})

它会自动的找到被index分离出去的chunk,并完成引用

目前这个版本仍处于测试解决,还未正式发布

原理

自动分包的原理其实并不复杂,主要经过以下步骤:

  1. 检查每个chunk编译的结果
  2. 根据分包策略,找到那些满足策略的模块
  3. 根据分包策略,生成新的chunk打包这些模块(代码有所变化)
  4. 把打包出去的模块从原始包中移除,并修正原始包代码

在代码层面,有以下变动

  1. 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块的代码
  2. 原始包的代码中,使用数组中的公共代码
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  mode: "production",
  entry: {
    page1: "./src/page1",
    page2: "./src/page2"
  },
  output: {
    filename: "[name].[hash:5].js"
  },
  optimization: {
    splitChunks: {
      //分包配置
      chunks: "all",
      // maxSize: 60000
      // automaticNameDelimiter: ".",
      // minChunks: 1,
      // minSize: 0
      cacheGroups: {
        styles: {
          minSize: 0,
          test: /\.css$/,
          minChunks: 2
        }
      }
    }
  },
  module: {
    rules: [
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      chunkFilename: "common.[hash:5].css"
    }),
    new HtmlWebpackPlugin({
      template:"./public/index.html",
      chunks: ["page1"]
    })
  ],
  stats: {
    colors: true,
    chunks: false,
    modules: false
  }
};

-------------------华丽分割线---------------------------

代码压缩

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin")
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  mode: "production",
  module: {
    rules: [
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }
    ]
  },
  optimization: {
    // 是否要启用压缩,默认情况下,生产环境会自动开启
    // minimize: true,
    minimizer: [
      // 压缩时使用的插件,可以有多个
      new TerserPlugin(),
      new OptimizeCSSAssetsPlugin()
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css"
    }),
    new HtmlWebpackPlugin()
  ],
  stats: {
    colors: true,
    chunks: false,
    modules: false
  }
};

-------------------华丽分割线---------------------------

tree shaking {ignore}

压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码

转存失败,建议直接上传图片文件

背景

某些模块导出的代码并不一定会被用到

// myMath.js
export function add(a, b){
  console.log("add")
  return a+b;
}

export function sub(a, b){
  console.log("sub")
  return a-b;
}
// index.js
import {add} from "./myMath"
console.log(add(1,2));

tree shaking 用于移除掉不会用到的导出

使用

webpack2开始就支持了tree shaking

只要是生产环境,tree shaking自动开启

原理

webpack会从入口模块出发寻找依赖关系

当解析一个模块时,webpack会根据ES6的模块导入语句来判断,该模块依赖了另一个模块的哪个导出

webpack之所以选择ES6的模块导入语句,是因为ES6模块有以下特点:

  1. 导入导出语句只能是顶层语句
  2. import的模块名只能是字符串常量
  3. import绑定的变量是不可变的

这些特征都非常有利于分析出稳定的依赖

在具体分析依赖时,webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking

所以,如果你依赖的是一个导出的对象,由于JS语言的动态特性,以及webpack还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息

因此,我们在编写代码的时候,尽量

  • 使用export xxx导出,而不使用export default {xxx}导出
  • 使用import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入

依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记其他导出为dead code,然后交给代码压缩工具处理

代码压缩工具最终移除掉那些dead code代码

使用第三方库

某些第三方库可能使用的是commonjs的方式导出,比如lodash

又或者没有提供普通的ES6方式导出

对于这些库,tree shaking是无法发挥作用的

因此要寻找这些库的es6版本,好在很多流行但没有使用的ES6的第三方库,都发布了它的ES6版本,比如lodash-es

作用域分析

tree shaking本身并没有完善的作用域分析,可能导致在一些dead code函数中的依赖仍然会被视为依赖

插件webpack-deep-scope-plugin提供了作用域分析,可解决这些问题

副作用问题

webpack在tree shaking的使用,有一个原则:一定要保证代码正确运行

在满足该原则的基础上,再来决定如何tree shaking

因此,当webpack无法确定某个模块是否有副作用时,它往往将其视为有副作用

因此,某些情况可能并不是我们所想要的

//common.js
var n  = Math.random();

//index.js
import "./common.js"

虽然我们根本没用有common.js的导出,但webpack担心common.js有副作用,如果去掉会影响某些功能

如果要解决该问题,就需要标记该文件是没有副作用的

package.json中加入sideEffects

{
    "sideEffects": false
}

有两种配置方式:

  • false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些css文件的导入
  • 数组:设置哪些文件拥有副作用,例如:["!src/common.js"],表示只要不是src/common.js的文件,都有副作用

这种方式我们一般不处理,通常是一些第三方库在它们自己的package.json中标注

css tree shaking

webpack无法对css完成tree shaking,因为csses6没有半毛钱关系

因此对csstree shaking需要其他插件完成

例如:purgecss-webpack-plugin

注意:purgecss-webpack-plugincss module无能为力

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const DeepScope = require("webpack-deep-scope-plugin").default;
const MiniCss = require("mini-css-extract-plugin");
const Purgecss = require("purgecss-webpack-plugin");
const path = require("path");
const globAll = require("glob-all");
const srcAbs = path.resolve(__dirname, "src"); //得到src的绝对路径
const htmlPath = path.resolve(__dirname, "public/index.html");
const paths = globAll.sync([`${srcAbs}**/*.js`, htmlPath]);

module.exports = {
  mode: "production",
  module: {
    rules: [{ test: /\.css$/, use: [MiniCss.loader, "css-loader"] }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new DeepScope(),
    new MiniCss(),
    new Purgecss({
      paths
    })
  ]
};

-------------------华丽分割线---------------------------

懒加载

index.js

const btn = document.querySelector("button");
btn.onclick = async function() {
  //动态加载
  //import 是ES6的草案
  //浏览器会使用JSOP的方式远程去读取一个js模块
  //import()会返回一个promise   (* as obj)
  // const { chunk } = await import(/* webpackChunkName:"lodash" */"lodash-es");
  const { chunk } = await import("./util");
  const result = chunk([3, 5, 6, 7, 87], 2);
  console.log(result);
};

util.js

export { chunk } from "lodash-es";

-------------------华丽分割线---------------------------

BundleAnalyzer代码分析

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const WebpackBundleAnalyzer = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;

module.exports = {
  mode: "production",
  optimization: {
    splitChunks: {
      chunks: "all"
    }
  },
  plugins: [new CleanWebpackPlugin(), new WebpackBundleAnalyzer()]
};

-------------------华丽分割线---------------------------

gzip

gzip是一种压缩文件的算法

B/S结构中的压缩传输

2020-02-28-15-37-26.png

优点:传输效率可能得到大幅提升

缺点:服务器的压缩需要时间,客户端的解压需要时间

使用webpack进行预压缩

使用compression-webpack-plugin插件对打包结果进行预压缩,可以移除服务器的压缩时间

2020-02-28-15-41-44.png