Repository: webpack/webpack (JavaScript)

Issue:
`BannerPlugin` does not work as expected with `watch: true` and `mode: 'development'`
# Bug report

**What is the current behavior?**

Hi,

The issue occurs with `watch: true` and `mode: 'development'`. This **causes a serious PERFORMANCE PROBLEM** when we have many entry points like me (build during watching takes ~60s instead of ~2s).

Please note that, in my example, the files don't import each other.

**WITHOUT** the `BannerPlugin` plugin, only the changed file is emitted: this is the expected behavior. On the screenshot (step 2), you can see that I have only changed the file `view/test/a.js` and only `view/test/a.js` has been emitted.

![without_banner_plugin](https://user-images.githubusercontent.com/59090546/155859334-e795484a-9df5-4cd4-912e-330b3611919f.png)

**WITH** the `BannerPlugin` plugin, all files are emitted when a single file is changed: this **ISN'T** the expected behavior. On the screenshot (step 2), you can see that I have only changed the file `view/test/a.js` and both `view/test/a.js` and `view/test/b.js` have been emitted.

![with_banner_plugin](https://user-images.githubusercontent.com/59090546/155859348-830cb204-82b2-429d-a30e-fb9afd5ad713.png)

Results:
| `BannerPlugin` enabled | Emitted files    | Is expected behavior? |
|------------------------|------------------|-----------------------|
| NO                     | The changed file | YES                    |
| YES                    | All files        | NO _(ISSUE !!!)_                |

**If the current behavior is a bug, please provide the steps to reproduce.**

You can use [this repository](https://github.com/BrokenSourceCode/webpack-bannerplugin-issue) that I created on purpose to reproduce the bug. Please comment [this line](https://github.com/BrokenSourceCode/webpack-bannerplugin-issue/blob/main/webpack.config.js#L17) if you want to get the expected behavior.

`webpack.config.js`:
```js
import path from 'path';
import webpack from 'webpack';
import WebpackBar from 'webpackbar';

export default {
	mode: 'development',
	devtool: 'source-map',
	watch: true,
	entry: {
		'view/test/a': path.resolve('view/test/a.js'),
		'view/test/b': path.resolve('view/test/b.js')
	},
	output: {
		path: path.resolve('./dist')
	},
	plugins: [
		new webpack.BannerPlugin({banner: 'My license'}), // If we comment this line, we get the expected behavior.
		new WebpackBar({
			reporters: [
				'fancy',
				'stats'
			]
		})
	]
};
```

**What is the expected behavior?**

**WITH** the `BannerPlugin` plugin, only the changed file should be emitted (exactly like without the plugin).

**Other relevant information:**
webpack version:  5.69.1
Node.js version: 16.13.2
Operating System: Windows 10



Hints:
Yep, we need a WeakMap cache in the BannerPlugin. Maybe `WeakMap<Source, { comment: string, result: Source }>`.

Patch:
diff --git a/lib/BannerPlugin.js b/lib/BannerPlugin.js
--- a/lib/BannerPlugin.js
+++ b/lib/BannerPlugin.js
@@ -77,6 +77,7 @@ class BannerPlugin {
 			undefined,
 			options
 		);
+		const cache = new WeakMap();
 
 		compiler.hooks.compilation.tap("BannerPlugin", compilation => {
 			compilation.hooks.processAssets.tap(
@@ -102,10 +103,15 @@ class BannerPlugin {
 
 							const comment = compilation.getPath(banner, data);
 
-							compilation.updateAsset(
-								file,
-								old => new ConcatSource(comment, "\n", old)
-							);
+							compilation.updateAsset(file, old => {
+								let cached = cache.get(old);
+								if (!cached || cached.comment !== comment) {
+									const source = new ConcatSource(comment, "\n", old);
+									cache.set(old, { source, comment });
+									return source;
+								}
+								return cached.source;
+							});
 						}
 					}
 				}


Test patch:
diff --git a/test/BannerPlugin.test.js b/test/BannerPlugin.test.js
new file mode 100644
--- /dev/null
+++ b/test/BannerPlugin.test.js
@@ -0,0 +1,41 @@
+"use strict";
+
+const path = require("path");
+const fs = require("graceful-fs");
+
+const webpack = require("..");
+
+it("should cache assets", done => {
+	const entry1File = path.join(__dirname, "js", "BannerPlugin", "entry1.js");
+	const entry2File = path.join(__dirname, "js", "BannerPlugin", "entry2.js");
+	try {
+		fs.mkdirSync(path.join(__dirname, "js", "BannerPlugin"), {
+			recursive: true
+		});
+	} catch (e) {
+		// empty
+	}
+	const compiler = webpack({
+		mode: "development",
+		entry: {
+			entry1: entry1File,
+			entry2: entry2File
+		},
+		output: {
+			path: path.join(__dirname, "js", "BannerPlugin", "output")
+		},
+		plugins: [new webpack.BannerPlugin("banner is a string")]
+	});
+	fs.writeFileSync(entry1File, "1", "utf-8");
+	fs.writeFileSync(entry2File, "1", "utf-8");
+	compiler.run(err => {
+		if (err) return done(err);
+		fs.writeFileSync(entry2File, "2", "utf-8");
+		compiler.run((err, stats) => {
+			const { assets } = stats.toJson();
+			expect(assets.find(as => as.name === "entry1.js").emitted).toBe(false);
+			expect(assets.find(as => as.name === "entry2.js").emitted).toBe(true);
+			done(err);
+		});
+	});
+});