diff --git a/package.json b/package.json
index 6df445f290..e87be0ab21 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
 		"autwh": "0.0.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
+		"cache-loader": "^1.2.0",
 		"cafy": "3.2.1",
 		"chai": "4.1.2",
 		"chai-http": "3.0.0",
@@ -117,7 +118,7 @@
 		"gulp-typescript": "3.2.4",
 		"gulp-uglify": "3.0.0",
 		"gulp-util": "3.0.8",
-		"hard-source-webpack-plugin": "^0.5.18",
+		"hard-source-webpack-plugin": "0.6.0-alpha.8",
 		"highlight.js": "9.12.0",
 		"html-minifier": "^3.5.9",
 		"inquirer": "5.0.1",
diff --git a/src/web/app/desktop/views/components/context-menu-menu.vue b/src/web/app/desktop/views/components/context-menu-menu.vue
index c4ecc74a44..7e333d273c 100644
--- a/src/web/app/desktop/views/components/context-menu-menu.vue
+++ b/src/web/app/desktop/views/components/context-menu-menu.vue
@@ -2,13 +2,13 @@
 <ul class="me-nu">
 	<li v-for="(item, i) in menu" :key="i" :class="item.type">
 		<template v-if="item.type == 'item'">
-			<p @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
+			<p @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</p>
 		</template>
 		<template v-if="item.type == 'link'">
-			<a :href="item.href" :target="item.target" @click="click(item)"><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
+			<a :href="item.href" :target="item.target" @click="click(item)"><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}</a>
 		</template>
 		<template v-else-if="item.type == 'nest'">
-			<p><span class="icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
+			<p><span :class="$style.icon" v-if="item.icon" v-html="item.icon"></span>{{ item.text }}...<span class="caret">%fa:caret-right%</span></p>
 			<me-nu :menu="item.menu" @x="click"/>
 		</template>
 	</li>
@@ -41,7 +41,7 @@ export default Vue.extend({
 	li
 		display block
 
-		&:empty
+		&.divider
 			margin-top $padding
 			padding-top $padding
 			border-top solid 1px #eee
@@ -51,11 +51,14 @@ export default Vue.extend({
 				cursor default
 
 				> .caret
+					position absolute
+					top 0
+					right 8px
+
 					> *
-						position absolute
-						top 0
-						right 8px
 						line-height $item-height
+						width 28px
+						text-align center
 
 			&:hover > ul
 				visibility visible
@@ -80,12 +83,6 @@ export default Vue.extend({
 			*
 				pointer-events none
 
-			> .icon
-				> *
-					width 28px
-					margin-left -28px
-					text-align center
-
 		&:hover
 			> p, a
 				text-decoration none
@@ -112,3 +109,11 @@ export default Vue.extend({
 
 </style>
 
+<style lang="stylus" module>
+.icon
+	> *
+		width 28px
+		margin-left -28px
+		text-align center
+</style>
+
diff --git a/webpack/loaders/replace.js b/webpack/loaders/replace.js
index 4bb00a2abf..03cf1fcd78 100644
--- a/webpack/loaders/replace.js
+++ b/webpack/loaders/replace.js
@@ -1,17 +1,18 @@
 const loaderUtils = require('loader-utils');
 
-function trim(text) {
-	return text.substring(1, text.length - 2);
+function trim(text, g) {
+	return text.substring(1, text.length - (g ? 2 : 0));
 }
 
 module.exports = function(src) {
 	this.cacheable();
 	const options = loaderUtils.getOptions(this);
 	const search = options.search;
+	const g = search[search.length - 1] == 'g';
 	const replace = global[options.replace];
 	if (typeof search != 'string' || search.length == 0) console.error('invalid search');
 	if (typeof replace != 'function') console.error('invalid replacer:', replace, this.request);
-	src = src.replace(new RegExp(trim(search), 'g'), replace);
+	src = src.replace(new RegExp(trim(search, g), g ? 'g' : ''), replace);
 	this.callback(null, src);
 	return src;
 };
diff --git a/webpack/plugins/index.ts b/webpack/plugins/index.ts
index a29d2b7e2f..027f60224f 100644
--- a/webpack/plugins/index.ts
+++ b/webpack/plugins/index.ts
@@ -9,7 +9,7 @@ const isProduction = env === 'production';
 
 export default (version, lang) => {
 	const plugins = [
-		new HardSourceWebpackPlugin(),
+		//new HardSourceWebpackPlugin(),
 		consts(lang)
 	];
 
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index 9a85e91896..fae75059a6 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -2,6 +2,7 @@
  * webpack configuration
  */
 
+const minify = require('html-minifier').minify;
 import I18nReplacer from '../src/common/build/i18n';
 import { pattern as faPattern, replacement as faReplacement } from '../src/common/build/fa';
 const constants = require('../src/const.json');
@@ -13,6 +14,14 @@ import version from '../src/version';
 
 global['faReplacement'] = faReplacement;
 
+global['collapseSpacesReplacement'] = html => {
+	return minify(html, {
+		collapseWhitespace: true,
+		collapseInlineTagWhitespace: true,
+		keepClosingSlash: true
+	});
+};
+
 module.exports = Object.keys(langs).map(lang => {
 	// Chunk name
 	const name = lang;
@@ -44,7 +53,7 @@ module.exports = Object.keys(langs).map(lang => {
 			rules: [{
 				test: /\.vue$/,
 				exclude: /node_modules/,
-				use: [{
+				use: [/*'cache-loader', */{
 					loader: 'vue-loader',
 					options: {
 						cssSourceMap: false,
@@ -76,6 +85,12 @@ module.exports = Object.keys(langs).map(lang => {
 						search: faPattern.toString(),
 						replace: 'faReplacement'
 					}
+				}, {
+					loader: 'replace',
+					query: {
+						search: /^<template>([\s\S]+?)\r?\n<\/template>/.toString(),
+						replace: 'collapseSpacesReplacement'
+					}
 				}]
 			}, {
 				test: /\.styl$/,