Skip to content

Commit 3fc83c9

Browse files
committed
Add precompressed brotli/gzip assets
This uses webpack to emit .gz and .br files for each js/css file into the public directory. The benefit of this is that our assets are now always optimally compressed without any runtime performance impact. The backend will dynamically emit compressed public assets based on the client's content-encoding support and there is no inteference with the existing gzip option (which does slow runtime compression). Overall, this will increase bindata size and memory consumption by around 20 MiB while saving around 25-50% of transfer size for every affected asset.
1 parent 4d9f59a commit 3fc83c9

File tree

10 files changed

+196
-55
lines changed

10 files changed

+196
-55
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ COMMA := ,
2727

2828
XGO_VERSION := go-1.14.x
2929
MIN_GO_VERSION := 001012000
30-
MIN_NODE_VERSION := 010013000
30+
MIN_NODE_VERSION := 010016000
3131

3232
DOCKER_IMAGE ?= gitea/gitea
3333
DOCKER_TAG ?= latest
@@ -196,7 +196,7 @@ node-check:
196196
$(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | tr '.' ' ');))
197197
$(eval NPM_MISSING := $(shell hash npm > /dev/null 2>&1 || echo 1))
198198
@if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" -o "$(NPM_MISSING)" = "1" ]; then \
199-
echo "Gitea requires Node.js 10 or greater and npm to build. You can get it at https://nodejs.org/en/download/"; \
199+
echo "Gitea requires Node.js 10.16 or greater and npm to build. You can get it at https://nodejs.org/en/download/"; \
200200
exit 1; \
201201
fi
202202

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ or if sqlite support is required:
4141
The `build` target is split into two sub-targets:
4242

4343
- `make backend` which requires [Go 1.12](https://golang.org/dl/) or greater.
44-
- `make frontend` which requires [Node.js 10.13](https://nodejs.org/en/download/) or greater.
44+
- `make frontend` which requires [Node.js 10.16](https://nodejs.org/en/download/) or greater.
4545

4646
If pre-built frontend files are present it is possible to only build the backend:
4747

custom/conf/app.example.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ KEY_FILE = https/key.pem
318318
STATIC_ROOT_PATH =
319319
; Default path for App data
320320
APP_DATA_PATH = data
321-
; Application level GZIP support
321+
; Enable gzip compression for runtime-generated content
322322
ENABLE_GZIP = false
323323
; Application profiling (memory and cpu)
324324
; For "web" command it listens on localhost:6060

docs/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ params:
2121
version: 1.12.2
2222
minGoVersion: 1.12
2323
goVersion: 1.14
24-
minNodeVersion: 10.13
24+
minNodeVersion: 10.16
2525

2626
outputs:
2727
home:

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
203203
- `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. From 1.11 paths are relative to `CUSTOM_PATH`.
204204
- `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path.
205205
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars.
206-
- `ENABLE_GZIP`: **false**: Enables application-level GZIP support.
206+
- `ENABLE_GZIP`: **false**: Enable gzip compression for runtime-generated content.
207207
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login\].
208208
- `LFS_START_SERVER`: **false**: Enables git-lfs support.
209209
- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files.

modules/public/public.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"path"
1212
"path/filepath"
13+
"regexp"
1314
"strings"
1415
"time"
1516

@@ -89,6 +90,8 @@ func (opts *Options) staticHandler(dir string) macaron.Handler {
8990
}
9091
}
9192

93+
var commaRe = regexp.MustCompile(`,\s*`)
94+
9295
func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) bool {
9396
if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
9497
return false
@@ -106,7 +109,32 @@ func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options)
106109
}
107110
}
108111

109-
f, err := opt.FileSystem.Open(file)
112+
var f http.File
113+
split := commaRe.Split(ctx.Req.Header.Get("Accept-Encoding"), -1)
114+
encodings := make(map[string]bool)
115+
for i := range split {
116+
encodings[split[i]] = true
117+
}
118+
119+
var err error
120+
if encodings["br"] {
121+
f, err = opt.FileSystem.Open(file + ".br")
122+
if err != nil {
123+
f, err = opt.FileSystem.Open(file)
124+
} else {
125+
ctx.Resp.Header().Set("Content-Encoding", "br")
126+
}
127+
} else if encodings["gzip"] {
128+
f, err = opt.FileSystem.Open(file + ".gz")
129+
if err != nil {
130+
f, err = opt.FileSystem.Open(file)
131+
} else {
132+
ctx.Resp.Header().Set("Content-Encoding", "gzip")
133+
}
134+
} else {
135+
f, err = opt.FileSystem.Open(file)
136+
}
137+
110138
if err != nil {
111139
// 404 requests to any known entries in `public`
112140
if path.Base(opts.Directory) == "public" {

package-lock.json

Lines changed: 83 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"license": "MIT",
33
"private": true,
44
"engines": {
5-
"node": ">= 10.13.0"
5+
"node": ">= 10.16.0"
66
},
77
"dependencies": {
88
"@babel/core": "7.10.5",
@@ -14,6 +14,7 @@
1414
"@primer/octicons": "10.0.0",
1515
"babel-loader": "8.1.0",
1616
"clipboard": "2.0.6",
17+
"compression-webpack-plugin": "4.0.0",
1718
"core-js": "3.6.5",
1819
"css-loader": "4.0.0",
1920
"cssnano-webpack-plugin": "1.0.3",

routers/routes/routes.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,6 @@ func NewMacaron() *macaron.Macaron {
130130
setupAccessLogger(m)
131131
}
132132
m.Use(macaron.Recovery())
133-
if setting.EnableGzip {
134-
m.Use(gzip.Middleware())
135-
}
136133
if setting.Protocol == setting.FCGI || setting.Protocol == setting.FCGIUnix {
137134
m.SetURLPrefix(setting.AppSubURL)
138135
}
@@ -149,6 +146,9 @@ func NewMacaron() *macaron.Macaron {
149146
ExpiresAfter: setting.StaticCacheTime,
150147
},
151148
))
149+
if setting.EnableGzip {
150+
m.Use(gzip.Middleware())
151+
}
152152
m.Use(public.StaticHandler(
153153
setting.AvatarUploadPath,
154154
&public.Options{

webpack.config.js

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const fastGlob = require('fast-glob');
22
const wrapAnsi = require('wrap-ansi');
3+
const {constants} = require('zlib');
4+
const CompressionPlugin = require('compression-webpack-plugin');
35
const CssNanoPlugin = require('cssnano-webpack-plugin');
46
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
57
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
@@ -37,6 +39,75 @@ const filterCssImport = (url, ...args) => {
3739
return true;
3840
};
3941

42+
const plugins = [
43+
new VueLoaderPlugin(),
44+
// avoid generating useless js output files for css--only chunks
45+
new FixStyleOnlyEntriesPlugin({
46+
extensions: ['less', 'scss', 'css'],
47+
silent: true,
48+
}),
49+
new MiniCssExtractPlugin({
50+
filename: 'css/[name].css',
51+
chunkFilename: 'css/[name].css',
52+
}),
53+
new SourceMapDevToolPlugin({
54+
filename: '[file].map',
55+
include: [
56+
'js/index.js',
57+
'css/index.css',
58+
],
59+
}),
60+
new MonacoWebpackPlugin({
61+
filename: 'js/monaco-[name].worker.js',
62+
}),
63+
new LicenseWebpackPlugin({
64+
outputFilename: 'js/licenses.txt',
65+
perChunkOutput: false,
66+
addBanner: false,
67+
skipChildCompilers: true,
68+
modulesDirectories: [
69+
resolve(__dirname, 'node_modules'),
70+
],
71+
renderLicenses: (modules) => {
72+
const line = '-'.repeat(80);
73+
return modules.map((module) => {
74+
const {name, version} = module.packageJson;
75+
const {licenseId, licenseText} = module;
76+
const body = wrapAnsi(licenseText || '', 80);
77+
return `${line}\n${name}@${version} - ${licenseId}\n${line}\n${body}`;
78+
}).join('\n');
79+
},
80+
stats: {
81+
warnings: false,
82+
errors: true,
83+
},
84+
}),
85+
];
86+
87+
if (isProduction) {
88+
plugins.push(
89+
new CompressionPlugin({
90+
filename: '[path].gz',
91+
algorithm: 'gzip',
92+
test: /\.(js|css)$/,
93+
compressionOptions: {
94+
level: constants.Z_BEST_COMPRESSION,
95+
},
96+
threshold: 10240,
97+
}),
98+
new CompressionPlugin({
99+
filename: '[path].br',
100+
algorithm: 'brotliCompress',
101+
test: /\.(js|css)$/,
102+
compressionOptions: {
103+
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
104+
},
105+
threshold: 10240,
106+
}),
107+
);
108+
}
109+
110+
40111
module.exports = {
41112
mode: isProduction ? 'production' : 'development',
42113
entry: {
@@ -255,50 +326,7 @@ module.exports = {
255326
},
256327
],
257328
},
258-
plugins: [
259-
new VueLoaderPlugin(),
260-
// avoid generating useless js output files for css--only chunks
261-
new FixStyleOnlyEntriesPlugin({
262-
extensions: ['less', 'scss', 'css'],
263-
silent: true,
264-
}),
265-
new MiniCssExtractPlugin({
266-
filename: 'css/[name].css',
267-
chunkFilename: 'css/[name].css',
268-
}),
269-
new SourceMapDevToolPlugin({
270-
filename: '[file].map',
271-
include: [
272-
'js/index.js',
273-
'css/index.css',
274-
],
275-
}),
276-
new MonacoWebpackPlugin({
277-
filename: 'js/monaco-[name].worker.js',
278-
}),
279-
new LicenseWebpackPlugin({
280-
outputFilename: 'js/licenses.txt',
281-
perChunkOutput: false,
282-
addBanner: false,
283-
skipChildCompilers: true,
284-
modulesDirectories: [
285-
resolve(__dirname, 'node_modules'),
286-
],
287-
renderLicenses: (modules) => {
288-
const line = '-'.repeat(80);
289-
return modules.map((module) => {
290-
const {name, version} = module.packageJson;
291-
const {licenseId, licenseText} = module;
292-
const body = wrapAnsi(licenseText || '', 80);
293-
return `${line}\n${name}@${version} - ${licenseId}\n${line}\n${body}`;
294-
}).join('\n');
295-
},
296-
stats: {
297-
warnings: false,
298-
errors: true,
299-
},
300-
}),
301-
],
329+
plugins,
302330
performance: {
303331
hints: false,
304332
maxEntrypointSize: Infinity,
@@ -321,6 +349,7 @@ module.exports = {
321349
// exclude monaco's language chunks in stats output for brevity
322350
// https://github.com/microsoft/monaco-editor-webpack-plugin/issues/113
323351
/^js\/[0-9]+\.js$/,
352+
/\.(gz|br)$/,
324353
],
325354
},
326355
};

0 commit comments

Comments
 (0)