Closed digitaldan closed 1 year ago
bcc6234(current) vs c74728a main#582(baseline)
Metrics (no changes)
Current Job #583 |
Baseline Job #582 |
|
---|---|---|
Initial JS | 1.71MiB |
1.71MiB |
Initial CSS | 607.98KiB |
607.98KiB |
Cache Invalidation | 0% |
0% |
Chunks | 218 |
218 |
Assets | 687 |
687 |
Modules | 2008 |
2008 |
Duplicate Modules | 110 |
110 |
Duplicate Code | 1.82% |
1.82% |
Packages | 133 |
133 |
Duplicate Packages | 15 |
15 |
Hi @ghys, i have longed noticed that the browser does not seem to do a good job caching assets from our UI and static files. This is somewhat noticeable on on a local LAN, more so if the hardware is a little slow, and on an iffy cellular connection this can be quite noticeable.
This replaces the simple file serving we did have, which did not have a concept of caching, with a servlet who will either A) use the startup time of the bundle as the "last-modified" time, which ensures all bundled files get cached appropriately or B) use the last modified time of the local file in the user's 'html' directory, which is especially nice when dealing with large images.
I also added a hash to the webpack app bundles to ensure uniqueness when loading, with the above caching this may not be absolutely necessary, but seems like a good idea in any case.
@digitaldan that's great! A new servlet will help a lot for sure!
I'd like to have this PR as an opportunity to discuss further improvements and fixes:
I've found an implementation of a servlet that seems to tackle both: https://balusc.omnifaces.org/2009/02/fileservlet-supporting-resume-and.html Maybe it can be an inspiration in our servlet (notice it's LGPL-licensed).
For GZip compression the example above will perform the compression on the fly (with a java.util.zip.GZIPOutputStream
) for all assets (like static images), which is great, but the suggestion in the case of main UI assets (better imo) was to let webpack pre-generate the compressed assets and have the servlet just serve them as-is if they exist, with a Content-Encoding: gzip
or Content-Encoding: br
response header.
I've done that just now as a test, following https://github.com/webpack-contrib/compression-webpack-plugin/tree/v6.0.3#multiple-compressed-versions-of-assets-for-different-algorithm (changed minRatio
to Infinity
)
$ npm install compression-webpack-plugin@6.0.3 --save-dev
(compression-webpack-plugin@6.0.3 is the last version compatible with webpack 4)
The results are as follows, the potential gains are impressive:
Asset Size Chunks Chunk Names
./index.html 1.21 KiB [emitted]
css/0.app.css 69 bytes 0 [emitted]
css/0.app.css.br 43 bytes [emitted]
css/0.app.css.gz 85 bytes [emitted]
css/0.app.css.map 499 bytes [emitted]
css/14.app.css 1.88 KiB 14 [emitted] about-page
css/14.app.css.br 498 bytes [emitted]
css/14.app.css.gz 641 bytes [emitted]
css/14.app.css.map 3.8 KiB [emitted]
css/15.app.css 9.02 KiB 15 [emitted] admin-base
css/15.app.css.br 1.69 KiB [emitted]
css/15.app.css.gz 1.98 KiB [emitted]
css/15.app.css.map 17.4 KiB [emitted]
css/16.app.css 5.68 KiB 16 [emitted] admin-config
css/16.app.css.br 1.27 KiB [emitted]
css/16.app.css.gz 1.52 KiB [emitted]
css/16.app.css.map 14.4 KiB [emitted]
css/17.app.css 3.51 KiB 17 [emitted] admin-devtools
css/17.app.css.br 768 bytes [emitted]
css/17.app.css.gz 950 bytes [emitted]
css/17.app.css.map 6.22 KiB [emitted]
css/18.app.css 2.98 KiB 18 [emitted] admin-pages
css/18.app.css.br 745 bytes [emitted]
css/18.app.css.gz 887 bytes [emitted]
css/18.app.css.map 7.43 KiB [emitted]
css/19.app.css 2 KiB 19, 6 [emitted] admin-pages-echarts
css/19.app.css.br 447 bytes [emitted]
css/19.app.css.gz 561 bytes [emitted]
css/19.app.css.map 4.54 KiB [emitted]
css/20.app.css 2.99 KiB 20, 7 [emitted] admin-pages-leaflet
css/20.app.css.br 479 bytes [emitted]
css/20.app.css.gz 576 bytes [emitted]
css/20.app.css.map 6.06 KiB [emitted]
css/21.app.css 1.3 KiB 21 [emitted] admin-rules
css/21.app.css.br 407 bytes [emitted]
css/21.app.css.gz 524 bytes [emitted]
css/21.app.css.map 3.56 KiB [emitted]
css/22.app.css 127 bytes 22 [emitted] admin-schedule
css/22.app.css.br 79 bytes [emitted]
css/22.app.css.gz 105 bytes [emitted]
css/22.app.css.map 624 bytes [emitted]
css/23.app.css 583 bytes 23 [emitted] analyzer
css/23.app.css.br 237 bytes [emitted]
css/23.app.css.gz 288 bytes [emitted]
css/23.app.css.map 1.44 KiB [emitted]
css/24.app.css 1.17 KiB 24 [emitted] blockly-editor
css/24.app.css.br 331 bytes [emitted]
css/24.app.css.gz 427 bytes [emitted]
css/24.app.css.map 3.03 KiB [emitted]
css/25.app.css 1.1 KiB 25, 48 [emitted] config-parameter
css/25.app.css.br 366 bytes [emitted]
css/25.app.css.gz 495 bytes [emitted]
css/25.app.css.map 2.96 KiB [emitted]
css/26.app.css 175 bytes 26 [emitted] cronexpression-editor
css/26.app.css.br 92 bytes [emitted]
css/26.app.css.gz 129 bytes [emitted]
css/26.app.css.map 653 bytes [emitted]
css/27.app.css 1.42 KiB 27 [emitted] habot
css/27.app.css.br 366 bytes [emitted]
css/27.app.css.gz 502 bytes [emitted]
css/27.app.css.map 2.89 KiB [emitted]
css/3.app.css 14.6 KiB 3 [emitted] vendors~admin-pages-leaflet~location-picker~map-page~plan-page~script-editor
css/3.app.css.br 5.46 KiB [emitted]
css/3.app.css.gz 6.13 KiB [emitted]
css/3.app.css.map 23.1 KiB [emitted]
css/30.app.css 109 bytes 30 [emitted] oh-chart-component
css/30.app.css.br 49 bytes [emitted]
css/30.app.css.gz 94 bytes [emitted]
css/30.app.css.map 591 bytes [emitted]
css/33.app.css 875 bytes 33 [emitted] profile-page
css/33.app.css.br 212 bytes [emitted]
css/33.app.css.gz 303 bytes [emitted]
css/33.app.css.map 1.77 KiB [emitted]
css/34.app.css 14.8 KiB 34, 7 [emitted] script-editor
css/34.app.css.br 3.48 KiB [emitted]
css/34.app.css.gz 3.99 KiB [emitted]
css/34.app.css.map 27.1 KiB [emitted]
css/35.app.css 299 bytes 35 [emitted] setup-wizard
css/35.app.css.br 137 bytes [emitted]
css/35.app.css.gz 183 bytes [emitted]
css/35.app.css.map 847 bytes [emitted]
css/37.app.css 765 bytes 37 [emitted] vendors~canvas-layout
css/37.app.css.br 262 bytes [emitted]
css/37.app.css.gz 317 bytes [emitted]
css/37.app.css.map 1.48 KiB [emitted]
css/40.app.css 39.2 KiB 40 [emitted] vendors~oh-video-videojs
css/40.app.css.br 8.95 KiB [emitted]
css/40.app.css.gz 10.1 KiB [emitted]
css/40.app.css.map 56.4 KiB [emitted]
css/42.app.css 140 KiB 42 [emitted] vendors~swagger-css
css/42.app.css.br 15.6 KiB [emitted]
css/42.app.css.gz 21.6 KiB [emitted]
css/42.app.css.map 197 KiB [emitted]
css/45.app.css 234 bytes 45 [emitted] zwave-network
css/45.app.css.br 117 bytes [emitted]
css/45.app.css.gz 170 bytes [emitted]
css/45.app.css.map 801 bytes [emitted]
css/48.app.css 256 bytes 48 [emitted]
css/48.app.css.br 112 bytes [emitted]
css/48.app.css.gz 145 bytes [emitted]
css/48.app.css.map 758 bytes [emitted]
css/5.app.css 34 bytes 5 [emitted] vendors~admin-pages-echarts~oh-chart-component~zwave-network
css/5.app.css.br 36 bytes [emitted]
css/5.app.css.gz 51 bytes [emitted]
css/5.app.css.map 435 bytes [emitted]
css/6.app.css 753 bytes 6 [emitted] chart-page
css/6.app.css.br 171 bytes [emitted]
css/6.app.css.gz 224 bytes [emitted]
css/6.app.css.map 1.31 KiB [emitted]
css/7.app.css 360 bytes 7 [emitted] map-page
css/7.app.css.br 133 bytes [emitted]
css/7.app.css.gz 174 bytes [emitted]
css/7.app.css.map 851 bytes [emitted]
css/8.app.css 1.74 KiB 8 [emitted] plan-page
css/8.app.css.br 317 bytes [emitted]
css/8.app.css.gz 383 bytes [emitted]
css/8.app.css.map 2.73 KiB [emitted]
css/app.css 608 KiB 29 [emitted] main
css/app.css.br 66.5 KiB [emitted]
css/app.css.gz 86.9 KiB [emitted]
css/app.css.map 917 KiB [emitted]
fonts/Framework7Icons-Regular.ttf 292 KiB [emitted]
fonts/Framework7Icons-Regular.woff 142 KiB [emitted]
fonts/Framework7Icons-Regular.woff2 105 KiB [emitted]
fonts/MaterialIcons-Regular.ttf 311 KiB [emitted]
fonts/MaterialIcons-Regular.woff 144 KiB [emitted]
fonts/MaterialIcons-Regular.woff2 112 KiB [emitted]
images/openhab-logo-white.svg 17.7 KiB [emitted]
images/openhab-logo-white.svg.br 5.63 KiB [emitted]
images/openhab-logo.svg 17.7 KiB [emitted]
images/openhab-logo.svg.br 5.67 KiB [emitted]
js/0.app.035e53999bb6cb9d1b32.js 118 KiB 0 [emitted] [immutable]
js/0.app.035e53999bb6cb9d1b32.js.br 30.7 KiB [emitted] [immutable]
js/0.app.035e53999bb6cb9d1b32.js.gz 37.1 KiB [emitted] [immutable]
js/1.app.035e53999bb6cb9d1b32.js 21.8 KiB 1 [emitted] [immutable]
js/1.app.035e53999bb6cb9d1b32.js.br 6.06 KiB [emitted] [immutable]
js/1.app.035e53999bb6cb9d1b32.js.gz 6.75 KiB [emitted] [immutable]
js/10.app.035e53999bb6cb9d1b32.js 11.6 KiB 10 [emitted] [immutable] vendors~admin-config~admin-rules
js/10.app.035e53999bb6cb9d1b32.js.br 2.99 KiB [emitted] [immutable]
js/10.app.035e53999bb6cb9d1b32.js.gz 3.33 KiB [emitted] [immutable]
js/100.app.035e53999bb6cb9d1b32.js 968 bytes 100 [emitted] [immutable]
js/100.app.035e53999bb6cb9d1b32.js.br 546 bytes [emitted] [immutable]
js/100.app.035e53999bb6cb9d1b32.js.gz 568 bytes [emitted] [immutable]
js/101.app.035e53999bb6cb9d1b32.js 1 KiB 101 [emitted] [immutable]
js/101.app.035e53999bb6cb9d1b32.js.br 567 bytes [emitted] [immutable]
js/101.app.035e53999bb6cb9d1b32.js.gz 605 bytes [emitted] [immutable]
js/102.app.035e53999bb6cb9d1b32.js 1.02 KiB 102 [emitted] [immutable]
js/102.app.035e53999bb6cb9d1b32.js.br 570 bytes [emitted] [immutable]
js/102.app.035e53999bb6cb9d1b32.js.gz 619 bytes [emitted] [immutable]
js/103.app.035e53999bb6cb9d1b32.js 788 bytes 103 [emitted] [immutable]
js/103.app.035e53999bb6cb9d1b32.js.br 411 bytes [emitted] [immutable]
js/103.app.035e53999bb6cb9d1b32.js.gz 467 bytes [emitted] [immutable]
js/104.app.035e53999bb6cb9d1b32.js 832 bytes 104 [emitted] [immutable]
js/104.app.035e53999bb6cb9d1b32.js.br 421 bytes [emitted] [immutable]
js/104.app.035e53999bb6cb9d1b32.js.gz 474 bytes [emitted] [immutable]
js/105.app.035e53999bb6cb9d1b32.js 1.54 KiB 105 [emitted] [immutable]
js/105.app.035e53999bb6cb9d1b32.js.br 604 bytes [emitted] [immutable]
js/105.app.035e53999bb6cb9d1b32.js.gz 682 bytes [emitted] [immutable]
js/106.app.035e53999bb6cb9d1b32.js 1.34 KiB 106 [emitted] [immutable]
js/106.app.035e53999bb6cb9d1b32.js.br 513 bytes [emitted] [immutable]
js/106.app.035e53999bb6cb9d1b32.js.gz 619 bytes [emitted] [immutable]
js/107.app.035e53999bb6cb9d1b32.js 1.43 KiB 107 [emitted] [immutable]
js/107.app.035e53999bb6cb9d1b32.js.br 566 bytes [emitted] [immutable]
js/107.app.035e53999bb6cb9d1b32.js.gz 676 bytes [emitted] [immutable]
js/108.app.035e53999bb6cb9d1b32.js 1.16 KiB 108 [emitted] [immutable]
js/108.app.035e53999bb6cb9d1b32.js.br 632 bytes [emitted] [immutable]
js/108.app.035e53999bb6cb9d1b32.js.gz 674 bytes [emitted] [immutable]
js/109.app.035e53999bb6cb9d1b32.js 913 bytes 109 [emitted] [immutable]
js/109.app.035e53999bb6cb9d1b32.js.br 477 bytes [emitted] [immutable]
js/109.app.035e53999bb6cb9d1b32.js.gz 535 bytes [emitted] [immutable]
js/11.app.035e53999bb6cb9d1b32.js 16.9 KiB 11 [emitted] [immutable] vendors~admin-rules~config-parameter
js/11.app.035e53999bb6cb9d1b32.js.br 4.17 KiB [emitted] [immutable]
js/11.app.035e53999bb6cb9d1b32.js.gz 4.63 KiB [emitted] [immutable]
js/110.app.035e53999bb6cb9d1b32.js 1.42 KiB 110 [emitted] [immutable]
js/110.app.035e53999bb6cb9d1b32.js.br 576 bytes [emitted] [immutable]
js/110.app.035e53999bb6cb9d1b32.js.gz 622 bytes [emitted] [immutable]
js/111.app.035e53999bb6cb9d1b32.js 1.22 KiB 111 [emitted] [immutable]
js/111.app.035e53999bb6cb9d1b32.js.br 562 bytes [emitted] [immutable]
js/111.app.035e53999bb6cb9d1b32.js.gz 661 bytes [emitted] [immutable]
js/112.app.035e53999bb6cb9d1b32.js 945 bytes 112 [emitted] [immutable]
js/112.app.035e53999bb6cb9d1b32.js.br 495 bytes [emitted] [immutable]
js/112.app.035e53999bb6cb9d1b32.js.gz 553 bytes [emitted] [immutable]
js/113.app.035e53999bb6cb9d1b32.js 803 bytes 113 [emitted] [immutable]
js/113.app.035e53999bb6cb9d1b32.js.br 429 bytes [emitted] [immutable]
js/113.app.035e53999bb6cb9d1b32.js.gz 483 bytes [emitted] [immutable]
js/114.app.035e53999bb6cb9d1b32.js 757 bytes 114 [emitted] [immutable]
js/114.app.035e53999bb6cb9d1b32.js.br 407 bytes [emitted] [immutable]
js/114.app.035e53999bb6cb9d1b32.js.gz 454 bytes [emitted] [immutable]
js/115.app.035e53999bb6cb9d1b32.js 955 bytes 115 [emitted] [immutable]
js/115.app.035e53999bb6cb9d1b32.js.br 504 bytes [emitted] [immutable]
js/115.app.035e53999bb6cb9d1b32.js.gz 562 bytes [emitted] [immutable]
js/116.app.035e53999bb6cb9d1b32.js 1.05 KiB 116 [emitted] [immutable]
js/116.app.035e53999bb6cb9d1b32.js.br 488 bytes [emitted] [immutable]
js/116.app.035e53999bb6cb9d1b32.js.gz 556 bytes [emitted] [immutable]
js/117.app.035e53999bb6cb9d1b32.js 975 bytes 117 [emitted] [immutable]
js/117.app.035e53999bb6cb9d1b32.js.br 511 bytes [emitted] [immutable]
js/117.app.035e53999bb6cb9d1b32.js.gz 562 bytes [emitted] [immutable]
js/118.app.035e53999bb6cb9d1b32.js 1.47 KiB 118 [emitted] [immutable]
js/118.app.035e53999bb6cb9d1b32.js.br 570 bytes [emitted] [immutable]
js/118.app.035e53999bb6cb9d1b32.js.gz 654 bytes [emitted] [immutable]
js/119.app.035e53999bb6cb9d1b32.js 1.17 KiB 119 [emitted] [immutable]
js/119.app.035e53999bb6cb9d1b32.js.br 542 bytes [emitted] [immutable]
js/119.app.035e53999bb6cb9d1b32.js.gz 643 bytes [emitted] [immutable]
js/12.app.035e53999bb6cb9d1b32.js 114 KiB 12 [emitted] [immutable] vendors~oh-chart-component~zwave-network
js/12.app.035e53999bb6cb9d1b32.js.br 32.9 KiB [emitted] [immutable]
js/12.app.035e53999bb6cb9d1b32.js.gz 37.4 KiB [emitted] [immutable]
js/120.app.035e53999bb6cb9d1b32.js 1.38 KiB 120 [emitted] [immutable]
js/120.app.035e53999bb6cb9d1b32.js.br 544 bytes [emitted] [immutable]
js/120.app.035e53999bb6cb9d1b32.js.gz 646 bytes [emitted] [immutable]
js/121.app.035e53999bb6cb9d1b32.js 1.57 KiB 121 [emitted] [immutable]
js/121.app.035e53999bb6cb9d1b32.js.br 590 bytes [emitted] [immutable]
js/121.app.035e53999bb6cb9d1b32.js.gz 681 bytes [emitted] [immutable]
js/122.app.035e53999bb6cb9d1b32.js 1.07 KiB 122 [emitted] [immutable]
js/122.app.035e53999bb6cb9d1b32.js.br 483 bytes [emitted] [immutable]
js/122.app.035e53999bb6cb9d1b32.js.gz 559 bytes [emitted] [immutable]
js/123.app.035e53999bb6cb9d1b32.js 1.44 KiB 123 [emitted] [immutable]
js/123.app.035e53999bb6cb9d1b32.js.br 578 bytes [emitted] [immutable]
js/123.app.035e53999bb6cb9d1b32.js.gz 648 bytes [emitted] [immutable]
js/124.app.035e53999bb6cb9d1b32.js 1.17 KiB 124 [emitted] [immutable]
js/124.app.035e53999bb6cb9d1b32.js.br 551 bytes [emitted] [immutable]
js/124.app.035e53999bb6cb9d1b32.js.gz 647 bytes [emitted] [immutable]
js/125.app.035e53999bb6cb9d1b32.js 800 bytes 125 [emitted] [immutable]
js/125.app.035e53999bb6cb9d1b32.js.br 423 bytes [emitted] [immutable]
js/125.app.035e53999bb6cb9d1b32.js.gz 476 bytes [emitted] [immutable]
js/126.app.035e53999bb6cb9d1b32.js 1.41 KiB 126 [emitted] [immutable]
js/126.app.035e53999bb6cb9d1b32.js.br 541 bytes [emitted] [immutable]
js/126.app.035e53999bb6cb9d1b32.js.gz 635 bytes [emitted] [immutable]
js/127.app.035e53999bb6cb9d1b32.js 1.7 KiB 127 [emitted] [immutable]
js/127.app.035e53999bb6cb9d1b32.js.br 693 bytes [emitted] [immutable]
js/127.app.035e53999bb6cb9d1b32.js.gz 753 bytes [emitted] [immutable]
js/128.app.035e53999bb6cb9d1b32.js 1010 bytes 128 [emitted] [immutable]
js/128.app.035e53999bb6cb9d1b32.js.br 529 bytes [emitted] [immutable]
js/128.app.035e53999bb6cb9d1b32.js.gz 579 bytes [emitted] [immutable]
js/129.app.035e53999bb6cb9d1b32.js 756 bytes 129 [emitted] [immutable]
js/129.app.035e53999bb6cb9d1b32.js.br 400 bytes [emitted] [immutable]
js/129.app.035e53999bb6cb9d1b32.js.gz 448 bytes [emitted] [immutable]
js/13.app.035e53999bb6cb9d1b32.js 132 KiB 13 [emitted] [immutable]
js/13.app.035e53999bb6cb9d1b32.js.br 36.9 KiB [emitted] [immutable]
js/13.app.035e53999bb6cb9d1b32.js.gz 42.1 KiB [emitted] [immutable]
js/130.app.035e53999bb6cb9d1b32.js 1 KiB 130 [emitted] [immutable]
js/130.app.035e53999bb6cb9d1b32.js.br 550 bytes [emitted] [immutable]
js/130.app.035e53999bb6cb9d1b32.js.gz 597 bytes [emitted] [immutable]
js/131.app.035e53999bb6cb9d1b32.js 1.15 KiB 131 [emitted] [immutable]
js/131.app.035e53999bb6cb9d1b32.js.br 548 bytes [emitted] [immutable]
js/131.app.035e53999bb6cb9d1b32.js.gz 649 bytes [emitted] [immutable]
js/132.app.035e53999bb6cb9d1b32.js 1.66 KiB 132 [emitted] [immutable]
js/132.app.035e53999bb6cb9d1b32.js.br 635 bytes [emitted] [immutable]
js/132.app.035e53999bb6cb9d1b32.js.gz 734 bytes [emitted] [immutable]
js/133.app.035e53999bb6cb9d1b32.js 1.26 KiB 133 [emitted] [immutable]
js/133.app.035e53999bb6cb9d1b32.js.br 554 bytes [emitted] [immutable]
js/133.app.035e53999bb6cb9d1b32.js.gz 642 bytes [emitted] [immutable]
js/134.app.035e53999bb6cb9d1b32.js 1.22 KiB 134 [emitted] [immutable]
js/134.app.035e53999bb6cb9d1b32.js.br 499 bytes [emitted] [immutable]
js/134.app.035e53999bb6cb9d1b32.js.gz 568 bytes [emitted] [immutable]
js/135.app.035e53999bb6cb9d1b32.js 939 bytes 135 [emitted] [immutable]
js/135.app.035e53999bb6cb9d1b32.js.br 490 bytes [emitted] [immutable]
js/135.app.035e53999bb6cb9d1b32.js.gz 549 bytes [emitted] [immutable]
js/136.app.035e53999bb6cb9d1b32.js 924 bytes 136 [emitted] [immutable]
js/136.app.035e53999bb6cb9d1b32.js.br 478 bytes [emitted] [immutable]
js/136.app.035e53999bb6cb9d1b32.js.gz 538 bytes [emitted] [immutable]
js/137.app.035e53999bb6cb9d1b32.js 954 bytes 137 [emitted] [immutable]
js/137.app.035e53999bb6cb9d1b32.js.br 545 bytes [emitted] [immutable]
js/137.app.035e53999bb6cb9d1b32.js.gz 594 bytes [emitted] [immutable]
js/138.app.035e53999bb6cb9d1b32.js 1.47 KiB 138 [emitted] [immutable]
js/138.app.035e53999bb6cb9d1b32.js.br 580 bytes [emitted] [immutable]
js/138.app.035e53999bb6cb9d1b32.js.gz 690 bytes [emitted] [immutable]
js/139.app.035e53999bb6cb9d1b32.js 971 bytes 139 [emitted] [immutable]
js/139.app.035e53999bb6cb9d1b32.js.br 498 bytes [emitted] [immutable]
js/139.app.035e53999bb6cb9d1b32.js.gz 551 bytes [emitted] [immutable]
js/14.app.035e53999bb6cb9d1b32.js 98.1 KiB 14 [emitted] [immutable] about-page
js/14.app.035e53999bb6cb9d1b32.js.br 20.8 KiB [emitted] [immutable]
js/14.app.035e53999bb6cb9d1b32.js.gz 25.4 KiB [emitted] [immutable]
js/140.app.035e53999bb6cb9d1b32.js 1.6 KiB 140 [emitted] [immutable]
js/140.app.035e53999bb6cb9d1b32.js.br 642 bytes [emitted] [immutable]
js/140.app.035e53999bb6cb9d1b32.js.gz 736 bytes [emitted] [immutable]
js/141.app.035e53999bb6cb9d1b32.js 978 bytes 141 [emitted] [immutable]
js/141.app.035e53999bb6cb9d1b32.js.br 520 bytes [emitted] [immutable]
js/141.app.035e53999bb6cb9d1b32.js.gz 567 bytes [emitted] [immutable]
js/142.app.035e53999bb6cb9d1b32.js 970 bytes 142 [emitted] [immutable]
js/142.app.035e53999bb6cb9d1b32.js.br 498 bytes [emitted] [immutable]
js/142.app.035e53999bb6cb9d1b32.js.gz 554 bytes [emitted] [immutable]
js/143.app.035e53999bb6cb9d1b32.js 961 bytes 143 [emitted] [immutable]
js/143.app.035e53999bb6cb9d1b32.js.br 493 bytes [emitted] [immutable]
js/143.app.035e53999bb6cb9d1b32.js.gz 548 bytes [emitted] [immutable]
js/144.app.035e53999bb6cb9d1b32.js 965 bytes 144 [emitted] [immutable]
js/144.app.035e53999bb6cb9d1b32.js.br 511 bytes [emitted] [immutable]
js/144.app.035e53999bb6cb9d1b32.js.gz 568 bytes [emitted] [immutable]
js/145.app.035e53999bb6cb9d1b32.js 1.5 KiB 145 [emitted] [immutable]
js/145.app.035e53999bb6cb9d1b32.js.br 551 bytes [emitted] [immutable]
js/145.app.035e53999bb6cb9d1b32.js.gz 640 bytes [emitted] [immutable]
js/146.app.035e53999bb6cb9d1b32.js 1.42 KiB 146 [emitted] [immutable]
js/146.app.035e53999bb6cb9d1b32.js.br 730 bytes [emitted] [immutable]
js/146.app.035e53999bb6cb9d1b32.js.gz 798 bytes [emitted] [immutable]
js/147.app.035e53999bb6cb9d1b32.js 1010 bytes 147 [emitted] [immutable]
js/147.app.035e53999bb6cb9d1b32.js.br 519 bytes [emitted] [immutable]
js/147.app.035e53999bb6cb9d1b32.js.gz 580 bytes [emitted] [immutable]
js/148.app.035e53999bb6cb9d1b32.js 1020 bytes 148 [emitted] [immutable]
js/148.app.035e53999bb6cb9d1b32.js.br 524 bytes [emitted] [immutable]
js/148.app.035e53999bb6cb9d1b32.js.gz 584 bytes [emitted] [immutable]
js/149.app.035e53999bb6cb9d1b32.js 950 bytes 149 [emitted] [immutable]
js/149.app.035e53999bb6cb9d1b32.js.br 510 bytes [emitted] [immutable]
js/149.app.035e53999bb6cb9d1b32.js.gz 564 bytes [emitted] [immutable]
js/15.app.035e53999bb6cb9d1b32.js 61.6 KiB 15 [emitted] [immutable] admin-base
js/15.app.035e53999bb6cb9d1b32.js.br 11.7 KiB [emitted] [immutable]
js/15.app.035e53999bb6cb9d1b32.js.gz 14.4 KiB [emitted] [immutable]
js/150.app.035e53999bb6cb9d1b32.js 1.97 KiB 150 [emitted] [immutable]
js/150.app.035e53999bb6cb9d1b32.js.br 808 bytes [emitted] [immutable]
js/150.app.035e53999bb6cb9d1b32.js.gz 973 bytes [emitted] [immutable]
js/151.app.035e53999bb6cb9d1b32.js 783 bytes 151 [emitted] [immutable]
js/151.app.035e53999bb6cb9d1b32.js.br 443 bytes [emitted] [immutable]
js/151.app.035e53999bb6cb9d1b32.js.gz 487 bytes [emitted] [immutable]
js/152.app.035e53999bb6cb9d1b32.js 1.17 KiB 152 [emitted] [immutable]
js/152.app.035e53999bb6cb9d1b32.js.br 491 bytes [emitted] [immutable]
js/152.app.035e53999bb6cb9d1b32.js.gz 590 bytes [emitted] [immutable]
js/153.app.035e53999bb6cb9d1b32.js 1.09 KiB 153 [emitted] [immutable]
js/153.app.035e53999bb6cb9d1b32.js.br 598 bytes [emitted] [immutable]
js/153.app.035e53999bb6cb9d1b32.js.gz 626 bytes [emitted] [immutable]
js/154.app.035e53999bb6cb9d1b32.js 1.47 KiB 154 [emitted] [immutable]
js/154.app.035e53999bb6cb9d1b32.js.br 605 bytes [emitted] [immutable]
js/154.app.035e53999bb6cb9d1b32.js.gz 688 bytes [emitted] [immutable]
js/155.app.035e53999bb6cb9d1b32.js 1.47 KiB 155 [emitted] [immutable]
js/155.app.035e53999bb6cb9d1b32.js.br 674 bytes [emitted] [immutable]
js/155.app.035e53999bb6cb9d1b32.js.gz 739 bytes [emitted] [immutable]
js/156.app.035e53999bb6cb9d1b32.js 758 bytes 156 [emitted] [immutable]
js/156.app.035e53999bb6cb9d1b32.js.br 408 bytes [emitted] [immutable]
js/156.app.035e53999bb6cb9d1b32.js.gz 452 bytes [emitted] [immutable]
js/157.app.035e53999bb6cb9d1b32.js 951 bytes 157 [emitted] [immutable]
js/157.app.035e53999bb6cb9d1b32.js.br 510 bytes [emitted] [immutable]
js/157.app.035e53999bb6cb9d1b32.js.gz 558 bytes [emitted] [immutable]
js/158.app.035e53999bb6cb9d1b32.js 2.13 KiB 158 [emitted] [immutable]
js/158.app.035e53999bb6cb9d1b32.js.br 816 bytes [emitted] [immutable]
js/158.app.035e53999bb6cb9d1b32.js.gz 955 bytes [emitted] [immutable]
js/159.app.035e53999bb6cb9d1b32.js 1.73 KiB 159 [emitted] [immutable]
js/159.app.035e53999bb6cb9d1b32.js.br 757 bytes [emitted] [immutable]
js/159.app.035e53999bb6cb9d1b32.js.gz 817 bytes [emitted] [immutable]
js/16.app.035e53999bb6cb9d1b32.js 317 KiB 16 [emitted] [immutable] admin-config
js/16.app.035e53999bb6cb9d1b32.js.br 54.6 KiB [emitted] [immutable]
js/16.app.035e53999bb6cb9d1b32.js.gz 70.5 KiB [emitted] [immutable]
js/160.app.035e53999bb6cb9d1b32.js 1000 bytes 160 [emitted] [immutable]
js/160.app.035e53999bb6cb9d1b32.js.br 537 bytes [emitted] [immutable]
js/160.app.035e53999bb6cb9d1b32.js.gz 580 bytes [emitted] [immutable]
js/161.app.035e53999bb6cb9d1b32.js 1.03 KiB 161 [emitted] [immutable]
js/161.app.035e53999bb6cb9d1b32.js.br 539 bytes [emitted] [immutable]
js/161.app.035e53999bb6cb9d1b32.js.gz 595 bytes [emitted] [immutable]
js/162.app.035e53999bb6cb9d1b32.js 962 bytes 162 [emitted] [immutable]
js/162.app.035e53999bb6cb9d1b32.js.br 509 bytes [emitted] [immutable]
js/162.app.035e53999bb6cb9d1b32.js.gz 559 bytes [emitted] [immutable]
js/163.app.035e53999bb6cb9d1b32.js 1.82 KiB 163 [emitted] [immutable]
js/163.app.035e53999bb6cb9d1b32.js.br 611 bytes [emitted] [immutable]
js/163.app.035e53999bb6cb9d1b32.js.gz 702 bytes [emitted] [immutable]
js/164.app.035e53999bb6cb9d1b32.js 1.53 KiB 164 [emitted] [immutable]
js/164.app.035e53999bb6cb9d1b32.js.br 612 bytes [emitted] [immutable]
js/164.app.035e53999bb6cb9d1b32.js.gz 695 bytes [emitted] [immutable]
js/165.app.035e53999bb6cb9d1b32.js 946 bytes 165 [emitted] [immutable]
js/165.app.035e53999bb6cb9d1b32.js.br 478 bytes [emitted] [immutable]
js/165.app.035e53999bb6cb9d1b32.js.gz 544 bytes [emitted] [immutable]
js/166.app.035e53999bb6cb9d1b32.js 1.13 KiB 166 [emitted] [immutable]
js/166.app.035e53999bb6cb9d1b32.js.br 545 bytes [emitted] [immutable]
js/166.app.035e53999bb6cb9d1b32.js.gz 639 bytes [emitted] [immutable]
js/167.app.035e53999bb6cb9d1b32.js 1.49 KiB 167 [emitted] [immutable]
js/167.app.035e53999bb6cb9d1b32.js.br 619 bytes [emitted] [immutable]
js/167.app.035e53999bb6cb9d1b32.js.gz 709 bytes [emitted] [immutable]
js/168.app.035e53999bb6cb9d1b32.js 964 bytes 168 [emitted] [immutable]
js/168.app.035e53999bb6cb9d1b32.js.br 489 bytes [emitted] [immutable]
js/168.app.035e53999bb6cb9d1b32.js.gz 551 bytes [emitted] [immutable]
js/169.app.035e53999bb6cb9d1b32.js 975 bytes 169 [emitted] [immutable]
js/169.app.035e53999bb6cb9d1b32.js.br 489 bytes [emitted] [immutable]
js/169.app.035e53999bb6cb9d1b32.js.gz 555 bytes [emitted] [immutable]
js/17.app.035e53999bb6cb9d1b32.js 25.7 KiB 17 [emitted] [immutable] admin-devtools
js/17.app.035e53999bb6cb9d1b32.js.br 5.22 KiB [emitted] [immutable]
js/17.app.035e53999bb6cb9d1b32.js.gz 6.09 KiB [emitted] [immutable]
js/170.app.035e53999bb6cb9d1b32.js 1.01 KiB 170 [emitted] [immutable]
js/170.app.035e53999bb6cb9d1b32.js.br 398 bytes [emitted] [immutable]
js/170.app.035e53999bb6cb9d1b32.js.gz 440 bytes [emitted] [immutable]
js/171.app.035e53999bb6cb9d1b32.js 944 bytes 171 [emitted] [immutable]
js/171.app.035e53999bb6cb9d1b32.js.br 514 bytes [emitted] [immutable]
js/171.app.035e53999bb6cb9d1b32.js.gz 569 bytes [emitted] [immutable]
js/172.app.035e53999bb6cb9d1b32.js 794 bytes 172 [emitted] [immutable]
js/172.app.035e53999bb6cb9d1b32.js.br 433 bytes [emitted] [immutable]
js/172.app.035e53999bb6cb9d1b32.js.gz 483 bytes [emitted] [immutable]
js/173.app.035e53999bb6cb9d1b32.js 1.01 KiB 173 [emitted] [immutable]
js/173.app.035e53999bb6cb9d1b32.js.br 477 bytes [emitted] [immutable]
js/173.app.035e53999bb6cb9d1b32.js.gz 527 bytes [emitted] [immutable]
js/174.app.035e53999bb6cb9d1b32.js 1.6 KiB 174 [emitted] [immutable]
js/174.app.035e53999bb6cb9d1b32.js.br 544 bytes [emitted] [immutable]
js/174.app.035e53999bb6cb9d1b32.js.gz 604 bytes [emitted] [immutable]
js/175.app.035e53999bb6cb9d1b32.js 1.31 KiB 175 [emitted] [immutable]
js/175.app.035e53999bb6cb9d1b32.js.br 539 bytes [emitted] [immutable]
js/175.app.035e53999bb6cb9d1b32.js.gz 634 bytes [emitted] [immutable]
js/176.app.035e53999bb6cb9d1b32.js 1.99 KiB 176 [emitted] [immutable]
js/176.app.035e53999bb6cb9d1b32.js.br 851 bytes [emitted] [immutable]
js/176.app.035e53999bb6cb9d1b32.js.gz 992 bytes [emitted] [immutable]
js/177.app.035e53999bb6cb9d1b32.js 1.16 KiB 177 [emitted] [immutable]
js/177.app.035e53999bb6cb9d1b32.js.br 511 bytes [emitted] [immutable]
js/177.app.035e53999bb6cb9d1b32.js.gz 573 bytes [emitted] [immutable]
js/178.app.035e53999bb6cb9d1b32.js 951 bytes 178 [emitted] [immutable]
js/178.app.035e53999bb6cb9d1b32.js.br 482 bytes [emitted] [immutable]
js/178.app.035e53999bb6cb9d1b32.js.gz 540 bytes [emitted] [immutable]
js/179.app.035e53999bb6cb9d1b32.js 1.15 KiB 179 [emitted] [immutable]
js/179.app.035e53999bb6cb9d1b32.js.br 555 bytes [emitted] [immutable]
js/179.app.035e53999bb6cb9d1b32.js.gz 638 bytes [emitted] [immutable]
js/18.app.035e53999bb6cb9d1b32.js 98.5 KiB 18 [emitted] [immutable] admin-pages
js/18.app.035e53999bb6cb9d1b32.js.br 19.2 KiB [emitted] [immutable]
js/18.app.035e53999bb6cb9d1b32.js.gz 22.7 KiB [emitted] [immutable]
js/180.app.035e53999bb6cb9d1b32.js 1.08 KiB 180 [emitted] [immutable]
js/180.app.035e53999bb6cb9d1b32.js.br 515 bytes [emitted] [immutable]
js/180.app.035e53999bb6cb9d1b32.js.gz 593 bytes [emitted] [immutable]
js/181.app.035e53999bb6cb9d1b32.js 1.13 KiB 181 [emitted] [immutable]
js/181.app.035e53999bb6cb9d1b32.js.br 612 bytes [emitted] [immutable]
js/181.app.035e53999bb6cb9d1b32.js.gz 684 bytes [emitted] [immutable]
js/182.app.035e53999bb6cb9d1b32.js 1.11 KiB 182 [emitted] [immutable]
js/182.app.035e53999bb6cb9d1b32.js.br 552 bytes [emitted] [immutable]
js/182.app.035e53999bb6cb9d1b32.js.gz 615 bytes [emitted] [immutable]
js/183.app.035e53999bb6cb9d1b32.js 1.26 KiB 183 [emitted] [immutable]
js/183.app.035e53999bb6cb9d1b32.js.br 608 bytes [emitted] [immutable]
js/183.app.035e53999bb6cb9d1b32.js.gz 721 bytes [emitted] [immutable]
js/184.app.035e53999bb6cb9d1b32.js 996 bytes 184 [emitted] [immutable]
js/184.app.035e53999bb6cb9d1b32.js.br 487 bytes [emitted] [immutable]
js/184.app.035e53999bb6cb9d1b32.js.gz 583 bytes [emitted] [immutable]
js/185.app.035e53999bb6cb9d1b32.js 1.06 KiB 185 [emitted] [immutable]
js/185.app.035e53999bb6cb9d1b32.js.br 504 bytes [emitted] [immutable]
js/185.app.035e53999bb6cb9d1b32.js.gz 599 bytes [emitted] [immutable]
js/186.app.035e53999bb6cb9d1b32.js 1.26 KiB 186 [emitted] [immutable]
js/186.app.035e53999bb6cb9d1b32.js.br 607 bytes [emitted] [immutable]
js/186.app.035e53999bb6cb9d1b32.js.gz 719 bytes [emitted] [immutable]
js/187.app.035e53999bb6cb9d1b32.js 45.9 KiB 187 [emitted] [immutable]
js/187.app.035e53999bb6cb9d1b32.js.br 12.8 KiB [emitted] [immutable]
js/187.app.035e53999bb6cb9d1b32.js.gz 15.5 KiB [emitted] [immutable]
js/19.app.035e53999bb6cb9d1b32.js 83.4 KiB 19, 6 [emitted] [immutable] admin-pages-echarts
js/19.app.035e53999bb6cb9d1b32.js.br 18.9 KiB [emitted] [immutable]
js/19.app.035e53999bb6cb9d1b32.js.gz 21.7 KiB [emitted] [immutable]
js/2.app.035e53999bb6cb9d1b32.js 96.6 KiB 2 [emitted] [immutable] vendors~about-page~admin-config~admin-devtools~admin-pages~admin-pages-echarts~admin-pages-leaflet~a~ce773236
js/2.app.035e53999bb6cb9d1b32.js.br 24.4 KiB [emitted] [immutable]
js/2.app.035e53999bb6cb9d1b32.js.gz 28 KiB [emitted] [immutable]
js/20.app.035e53999bb6cb9d1b32.js 58.1 KiB 20, 7 [emitted] [immutable] admin-pages-leaflet
js/20.app.035e53999bb6cb9d1b32.js.br 11.5 KiB [emitted] [immutable]
js/20.app.035e53999bb6cb9d1b32.js.gz 13.3 KiB [emitted] [immutable]
js/21.app.035e53999bb6cb9d1b32.js 120 KiB 21 [emitted] [immutable] admin-rules
js/21.app.035e53999bb6cb9d1b32.js.br 22.2 KiB [emitted] [immutable]
js/21.app.035e53999bb6cb9d1b32.js.gz 26.1 KiB [emitted] [immutable]
js/22.app.035e53999bb6cb9d1b32.js 5.98 KiB 22 [emitted] [immutable] admin-schedule
js/22.app.035e53999bb6cb9d1b32.js.br 1.87 KiB [emitted] [immutable]
js/22.app.035e53999bb6cb9d1b32.js.gz 2.13 KiB [emitted] [immutable]
js/23.app.035e53999bb6cb9d1b32.js 144 KiB 23 [emitted] [immutable] analyzer
js/23.app.035e53999bb6cb9d1b32.js.br 21.7 KiB [emitted] [immutable]
js/23.app.035e53999bb6cb9d1b32.js.gz 27.3 KiB [emitted] [immutable]
js/24.app.035e53999bb6cb9d1b32.js 129 KiB 24 [emitted] [immutable] blockly-editor
js/24.app.035e53999bb6cb9d1b32.js.br 25 KiB [emitted] [immutable]
js/24.app.035e53999bb6cb9d1b32.js.gz 29.9 KiB [emitted] [immutable]
js/25.app.035e53999bb6cb9d1b32.js 36.8 KiB 25, 48 [emitted] [immutable] config-parameter
js/25.app.035e53999bb6cb9d1b32.js.br 6.62 KiB [emitted] [immutable]
js/25.app.035e53999bb6cb9d1b32.js.gz 7.46 KiB [emitted] [immutable]
js/26.app.035e53999bb6cb9d1b32.js 24.8 KiB 26 [emitted] [immutable] cronexpression-editor
js/26.app.035e53999bb6cb9d1b32.js.br 3.1 KiB [emitted] [immutable]
js/26.app.035e53999bb6cb9d1b32.js.gz 3.69 KiB [emitted] [immutable]
js/27.app.035e53999bb6cb9d1b32.js 29.1 KiB 27 [emitted] [immutable] habot
js/27.app.035e53999bb6cb9d1b32.js.br 7.75 KiB [emitted] [immutable]
js/27.app.035e53999bb6cb9d1b32.js.gz 9.24 KiB [emitted] [immutable]
js/28.app.035e53999bb6cb9d1b32.js 5.78 KiB 28 [emitted] [immutable] location-picker
js/28.app.035e53999bb6cb9d1b32.js.br 1.96 KiB [emitted] [immutable]
js/28.app.035e53999bb6cb9d1b32.js.gz 2.23 KiB [emitted] [immutable]
js/3.app.035e53999bb6cb9d1b32.js 157 KiB 3 [emitted] [immutable] vendors~admin-pages-leaflet~location-picker~map-page~plan-page~script-editor
js/3.app.035e53999bb6cb9d1b32.js.br 41.7 KiB [emitted] [immutable]
js/3.app.035e53999bb6cb9d1b32.js.gz 48.1 KiB [emitted] [immutable]
js/30.app.035e53999bb6cb9d1b32.js 21.4 KiB 30 [emitted] [immutable] oh-chart-component
js/30.app.035e53999bb6cb9d1b32.js.br 5.33 KiB [emitted] [immutable]
js/30.app.035e53999bb6cb9d1b32.js.gz 5.98 KiB [emitted] [immutable]
js/31.app.035e53999bb6cb9d1b32.js 1.34 KiB 31 [emitted] [immutable] oh-video-videojs
js/31.app.035e53999bb6cb9d1b32.js.br 576 bytes [emitted] [immutable]
js/31.app.035e53999bb6cb9d1b32.js.gz 675 bytes [emitted] [immutable]
js/32.app.035e53999bb6cb9d1b32.js 2.78 KiB 32 [emitted] [immutable] oh-video-webrtc
js/32.app.035e53999bb6cb9d1b32.js.br 1.08 KiB [emitted] [immutable]
js/32.app.035e53999bb6cb9d1b32.js.gz 1.27 KiB [emitted] [immutable]
js/33.app.035e53999bb6cb9d1b32.js 42.7 KiB 33 [emitted] [immutable] profile-page
js/33.app.035e53999bb6cb9d1b32.js.br 9.42 KiB [emitted] [immutable]
js/33.app.035e53999bb6cb9d1b32.js.gz 11.4 KiB [emitted] [immutable]
js/34.app.035e53999bb6cb9d1b32.js 760 KiB 34, 7 [emitted] [immutable] script-editor
js/34.app.035e53999bb6cb9d1b32.js.br 159 KiB [emitted] [immutable]
js/34.app.035e53999bb6cb9d1b32.js.gz 212 KiB [emitted] [immutable]
js/35.app.035e53999bb6cb9d1b32.js 99.9 KiB 35 [emitted] [immutable] setup-wizard
js/35.app.035e53999bb6cb9d1b32.js.br 21.5 KiB [emitted] [immutable]
js/35.app.035e53999bb6cb9d1b32.js.gz 25.9 KiB [emitted] [immutable]
js/36.app.035e53999bb6cb9d1b32.js 747 KiB 36 [emitted] [immutable] vendors~blockly-editor
js/36.app.035e53999bb6cb9d1b32.js.br 128 KiB [emitted] [immutable]
js/36.app.035e53999bb6cb9d1b32.js.gz 164 KiB [emitted] [immutable]
js/37.app.035e53999bb6cb9d1b32.js 54.1 KiB 37 [emitted] [immutable] vendors~canvas-layout
js/37.app.035e53999bb6cb9d1b32.js.br 13.9 KiB [emitted] [immutable]
js/37.app.035e53999bb6cb9d1b32.js.gz 16.2 KiB [emitted] [immutable]
js/38.app.035e53999bb6cb9d1b32.js 263 KiB 38 [emitted] [immutable] vendors~jssip
js/38.app.035e53999bb6cb9d1b32.js.br 44.2 KiB [emitted] [immutable]
js/38.app.035e53999bb6cb9d1b32.js.gz 54.9 KiB [emitted] [immutable]
js/39.app.035e53999bb6cb9d1b32.js 239 KiB 39, 74 [emitted] [immutable] vendors~oh-chart-component
js/39.app.035e53999bb6cb9d1b32.js.br 64.1 KiB [emitted] [immutable]
js/39.app.035e53999bb6cb9d1b32.js.gz 75.4 KiB [emitted] [immutable]
js/4.app.035e53999bb6cb9d1b32.js 42.7 KiB 4 [emitted] [immutable] vendors~admin-pages-leaflet~map-page~script-editor
js/4.app.035e53999bb6cb9d1b32.js.br 7.89 KiB [emitted] [immutable]
js/4.app.035e53999bb6cb9d1b32.js.gz 8.93 KiB [emitted] [immutable]
js/40.app.035e53999bb6cb9d1b32.js 557 KiB 40 [emitted] [immutable] vendors~oh-video-videojs
js/40.app.035e53999bb6cb9d1b32.js.br 130 KiB [emitted] [immutable]
js/40.app.035e53999bb6cb9d1b32.js.gz 154 KiB [emitted] [immutable]
js/41.app.035e53999bb6cb9d1b32.js 1.35 MiB 41 [emitted] [immutable] vendors~swagger
js/41.app.035e53999bb6cb9d1b32.js.br 277 KiB [emitted] [immutable]
js/41.app.035e53999bb6cb9d1b32.js.gz 424 KiB [emitted] [immutable]
js/42.app.035e53999bb6cb9d1b32.js 84 bytes 42 [emitted] [immutable] vendors~swagger-css
js/42.app.035e53999bb6cb9d1b32.js.br 86 bytes [emitted] [immutable]
js/42.app.035e53999bb6cb9d1b32.js.gz 87 bytes [emitted] [immutable]
js/43.app.035e53999bb6cb9d1b32.js 30.9 KiB 43 [emitted] [immutable] vendors~vue-qrcode
js/43.app.035e53999bb6cb9d1b32.js.br 9.86 KiB [emitted] [immutable]
js/43.app.035e53999bb6cb9d1b32.js.gz 11.1 KiB [emitted] [immutable]
js/44.app.035e53999bb6cb9d1b32.js 27.8 KiB 44 [emitted] [immutable] vendors~zwave-network
js/44.app.035e53999bb6cb9d1b32.js.br 8.48 KiB [emitted] [immutable]
js/44.app.035e53999bb6cb9d1b32.js.gz 9.4 KiB [emitted] [immutable]
js/45.app.035e53999bb6cb9d1b32.js 1.91 KiB 45 [emitted] [immutable] zwave-network
js/45.app.035e53999bb6cb9d1b32.js.br 870 bytes [emitted] [immutable]
js/45.app.035e53999bb6cb9d1b32.js.gz 1020 bytes [emitted] [immutable]
js/46.app.035e53999bb6cb9d1b32.js 641 KiB 46 [emitted] [immutable]
js/46.app.035e53999bb6cb9d1b32.js.br 78.8 KiB [emitted] [immutable]
js/46.app.035e53999bb6cb9d1b32.js.gz 183 KiB [emitted] [immutable]
js/47.app.035e53999bb6cb9d1b32.js 376 bytes 47 [emitted] [immutable]
js/47.app.035e53999bb6cb9d1b32.js.br 109 bytes [emitted] [immutable]
js/47.app.035e53999bb6cb9d1b32.js.gz 133 bytes [emitted] [immutable]
js/48.app.035e53999bb6cb9d1b32.js 2.68 KiB 48 [emitted] [immutable]
js/48.app.035e53999bb6cb9d1b32.js.br 931 bytes [emitted] [immutable]
js/48.app.035e53999bb6cb9d1b32.js.gz 1.04 KiB [emitted] [immutable]
js/49.app.035e53999bb6cb9d1b32.js 945 bytes 49 [emitted] [immutable]
js/49.app.035e53999bb6cb9d1b32.js.br 480 bytes [emitted] [immutable]
js/49.app.035e53999bb6cb9d1b32.js.gz 549 bytes [emitted] [immutable]
js/5.app.035e53999bb6cb9d1b32.js 372 KiB 5 [emitted] [immutable] vendors~admin-pages-echarts~oh-chart-component~zwave-network
js/5.app.035e53999bb6cb9d1b32.js.br 100 KiB [emitted] [immutable]
js/5.app.035e53999bb6cb9d1b32.js.gz 121 KiB [emitted] [immutable]
js/50.app.035e53999bb6cb9d1b32.js 1.25 KiB 50 [emitted] [immutable]
js/50.app.035e53999bb6cb9d1b32.js.br 555 bytes [emitted] [immutable]
js/50.app.035e53999bb6cb9d1b32.js.gz 659 bytes [emitted] [immutable]
js/51.app.035e53999bb6cb9d1b32.js 1.14 KiB 51 [emitted] [immutable]
js/51.app.035e53999bb6cb9d1b32.js.br 531 bytes [emitted] [immutable]
js/51.app.035e53999bb6cb9d1b32.js.gz 606 bytes [emitted] [immutable]
js/52.app.035e53999bb6cb9d1b32.js 1.12 KiB 52 [emitted] [immutable]
js/52.app.035e53999bb6cb9d1b32.js.br 506 bytes [emitted] [immutable]
js/52.app.035e53999bb6cb9d1b32.js.gz 603 bytes [emitted] [immutable]
js/53.app.035e53999bb6cb9d1b32.js 970 bytes 53 [emitted] [immutable]
js/53.app.035e53999bb6cb9d1b32.js.br 430 bytes [emitted] [immutable]
js/53.app.035e53999bb6cb9d1b32.js.gz 498 bytes [emitted] [immutable]
js/54.app.035e53999bb6cb9d1b32.js 1.13 KiB 54 [emitted] [immutable]
js/54.app.035e53999bb6cb9d1b32.js.br 513 bytes [emitted] [immutable]
js/54.app.035e53999bb6cb9d1b32.js.gz 613 bytes [emitted] [immutable]
js/55.app.035e53999bb6cb9d1b32.js 1.14 KiB 55 [emitted] [immutable]
js/55.app.035e53999bb6cb9d1b32.js.br 513 bytes [emitted] [immutable]
js/55.app.035e53999bb6cb9d1b32.js.gz 603 bytes [emitted] [immutable]
js/56.app.035e53999bb6cb9d1b32.js 1.14 KiB 56 [emitted] [immutable]
js/56.app.035e53999bb6cb9d1b32.js.br 523 bytes [emitted] [immutable]
js/56.app.035e53999bb6cb9d1b32.js.gz 609 bytes [emitted] [immutable]
js/57.app.035e53999bb6cb9d1b32.js 1.07 KiB 57 [emitted] [immutable]
js/57.app.035e53999bb6cb9d1b32.js.br 539 bytes [emitted] [immutable]
js/57.app.035e53999bb6cb9d1b32.js.gz 625 bytes [emitted] [immutable]
js/58.app.035e53999bb6cb9d1b32.js 990 bytes 58 [emitted] [immutable]
js/58.app.035e53999bb6cb9d1b32.js.br 520 bytes [emitted] [immutable]
js/58.app.035e53999bb6cb9d1b32.js.gz 576 bytes [emitted] [immutable]
js/59.app.035e53999bb6cb9d1b32.js 980 bytes 59 [emitted] [immutable]
js/59.app.035e53999bb6cb9d1b32.js.br 475 bytes [emitted] [immutable]
js/59.app.035e53999bb6cb9d1b32.js.gz 548 bytes [emitted] [immutable]
js/6.app.035e53999bb6cb9d1b32.js 738 bytes 6 [emitted] [immutable] chart-page
js/6.app.035e53999bb6cb9d1b32.js.br 362 bytes [emitted] [immutable]
js/6.app.035e53999bb6cb9d1b32.js.gz 427 bytes [emitted] [immutable]
js/60.app.035e53999bb6cb9d1b32.js 1.15 KiB 60 [emitted] [immutable]
js/60.app.035e53999bb6cb9d1b32.js.br 561 bytes [emitted] [immutable]
js/60.app.035e53999bb6cb9d1b32.js.gz 671 bytes [emitted] [immutable]
js/61.app.035e53999bb6cb9d1b32.js 918 bytes 61 [emitted] [immutable]
js/61.app.035e53999bb6cb9d1b32.js.br 463 bytes [emitted] [immutable]
js/61.app.035e53999bb6cb9d1b32.js.gz 523 bytes [emitted] [immutable]
js/62.app.035e53999bb6cb9d1b32.js 1.04 KiB 62 [emitted] [immutable]
js/62.app.035e53999bb6cb9d1b32.js.br 523 bytes [emitted] [immutable]
js/62.app.035e53999bb6cb9d1b32.js.gz 586 bytes [emitted] [immutable]
js/63.app.035e53999bb6cb9d1b32.js 1.53 KiB 63 [emitted] [immutable]
js/63.app.035e53999bb6cb9d1b32.js.br 578 bytes [emitted] [immutable]
js/63.app.035e53999bb6cb9d1b32.js.gz 674 bytes [emitted] [immutable]
js/64.app.035e53999bb6cb9d1b32.js 2.12 KiB 64 [emitted] [immutable]
js/64.app.035e53999bb6cb9d1b32.js.br 566 bytes [emitted] [immutable]
js/64.app.035e53999bb6cb9d1b32.js.gz 650 bytes [emitted] [immutable]
js/65.app.035e53999bb6cb9d1b32.js 1.26 KiB 65 [emitted] [immutable]
js/65.app.035e53999bb6cb9d1b32.js.br 663 bytes [emitted] [immutable]
js/65.app.035e53999bb6cb9d1b32.js.gz 718 bytes [emitted] [immutable]
js/66.app.035e53999bb6cb9d1b32.js 760 bytes 66 [emitted] [immutable]
js/66.app.035e53999bb6cb9d1b32.js.br 409 bytes [emitted] [immutable]
js/66.app.035e53999bb6cb9d1b32.js.gz 455 bytes [emitted] [immutable]
js/67.app.035e53999bb6cb9d1b32.js 1.06 KiB 67 [emitted] [immutable]
js/67.app.035e53999bb6cb9d1b32.js.br 544 bytes [emitted] [immutable]
js/67.app.035e53999bb6cb9d1b32.js.gz 606 bytes [emitted] [immutable]
js/68.app.035e53999bb6cb9d1b32.js 1.47 KiB 68 [emitted] [immutable]
js/68.app.035e53999bb6cb9d1b32.js.br 680 bytes [emitted] [immutable]
js/68.app.035e53999bb6cb9d1b32.js.gz 747 bytes [emitted] [immutable]
js/69.app.035e53999bb6cb9d1b32.js 1020 bytes 69 [emitted] [immutable]
js/69.app.035e53999bb6cb9d1b32.js.br 461 bytes [emitted] [immutable]
js/69.app.035e53999bb6cb9d1b32.js.gz 550 bytes [emitted] [immutable]
js/7.app.035e53999bb6cb9d1b32.js 9.09 KiB 7 [emitted] [immutable] map-page
js/7.app.035e53999bb6cb9d1b32.js.br 2.55 KiB [emitted] [immutable]
js/7.app.035e53999bb6cb9d1b32.js.gz 2.95 KiB [emitted] [immutable]
js/70.app.035e53999bb6cb9d1b32.js 957 bytes 70 [emitted] [immutable]
js/70.app.035e53999bb6cb9d1b32.js.br 541 bytes [emitted] [immutable]
js/70.app.035e53999bb6cb9d1b32.js.gz 569 bytes [emitted] [immutable]
js/71.app.035e53999bb6cb9d1b32.js 981 bytes 71 [emitted] [immutable]
js/71.app.035e53999bb6cb9d1b32.js.br 499 bytes [emitted] [immutable]
js/71.app.035e53999bb6cb9d1b32.js.gz 561 bytes [emitted] [immutable]
js/72.app.035e53999bb6cb9d1b32.js 988 bytes 72 [emitted] [immutable]
js/72.app.035e53999bb6cb9d1b32.js.br 506 bytes [emitted] [immutable]
js/72.app.035e53999bb6cb9d1b32.js.gz 564 bytes [emitted] [immutable]
js/73.app.035e53999bb6cb9d1b32.js 753 bytes 73 [emitted] [immutable]
js/73.app.035e53999bb6cb9d1b32.js.br 392 bytes [emitted] [immutable]
js/73.app.035e53999bb6cb9d1b32.js.gz 442 bytes [emitted] [immutable]
js/74.app.035e53999bb6cb9d1b32.js 1.2 KiB 74 [emitted] [immutable]
js/74.app.035e53999bb6cb9d1b32.js.br 622 bytes [emitted] [immutable]
js/74.app.035e53999bb6cb9d1b32.js.gz 673 bytes [emitted] [immutable]
js/75.app.035e53999bb6cb9d1b32.js 1.44 KiB 75 [emitted] [immutable]
js/75.app.035e53999bb6cb9d1b32.js.br 525 bytes [emitted] [immutable]
js/75.app.035e53999bb6cb9d1b32.js.gz 637 bytes [emitted] [immutable]
js/76.app.035e53999bb6cb9d1b32.js 1.26 KiB 76 [emitted] [immutable]
js/76.app.035e53999bb6cb9d1b32.js.br 579 bytes [emitted] [immutable]
js/76.app.035e53999bb6cb9d1b32.js.gz 698 bytes [emitted] [immutable]
js/77.app.035e53999bb6cb9d1b32.js 940 bytes 77 [emitted] [immutable]
js/77.app.035e53999bb6cb9d1b32.js.br 461 bytes [emitted] [immutable]
js/77.app.035e53999bb6cb9d1b32.js.gz 547 bytes [emitted] [immutable]
js/78.app.035e53999bb6cb9d1b32.js 931 bytes 78 [emitted] [immutable]
js/78.app.035e53999bb6cb9d1b32.js.br 453 bytes [emitted] [immutable]
js/78.app.035e53999bb6cb9d1b32.js.gz 539 bytes [emitted] [immutable]
js/79.app.035e53999bb6cb9d1b32.js 1020 bytes 79 [emitted] [immutable]
js/79.app.035e53999bb6cb9d1b32.js.br 509 bytes [emitted] [immutable]
js/79.app.035e53999bb6cb9d1b32.js.gz 591 bytes [emitted] [immutable]
js/8.app.035e53999bb6cb9d1b32.js 34 KiB 8 [emitted] [immutable] plan-page
js/8.app.035e53999bb6cb9d1b32.js.br 5.96 KiB [emitted] [immutable]
js/8.app.035e53999bb6cb9d1b32.js.gz 6.9 KiB [emitted] [immutable]
js/80.app.035e53999bb6cb9d1b32.js 935 bytes 80 [emitted] [immutable]
js/80.app.035e53999bb6cb9d1b32.js.br 456 bytes [emitted] [immutable]
js/80.app.035e53999bb6cb9d1b32.js.gz 545 bytes [emitted] [immutable]
js/81.app.035e53999bb6cb9d1b32.js 924 bytes 81 [emitted] [immutable]
js/81.app.035e53999bb6cb9d1b32.js.br 448 bytes [emitted] [immutable]
js/81.app.035e53999bb6cb9d1b32.js.gz 535 bytes [emitted] [immutable]
js/82.app.035e53999bb6cb9d1b32.js 1020 bytes 82 [emitted] [immutable]
js/82.app.035e53999bb6cb9d1b32.js.br 509 bytes [emitted] [immutable]
js/82.app.035e53999bb6cb9d1b32.js.gz 590 bytes [emitted] [immutable]
js/83.app.035e53999bb6cb9d1b32.js 1010 bytes 83 [emitted] [immutable]
js/83.app.035e53999bb6cb9d1b32.js.br 506 bytes [emitted] [immutable]
js/83.app.035e53999bb6cb9d1b32.js.gz 594 bytes [emitted] [immutable]
js/84.app.035e53999bb6cb9d1b32.js 936 bytes 84 [emitted] [immutable]
js/84.app.035e53999bb6cb9d1b32.js.br 457 bytes [emitted] [immutable]
js/84.app.035e53999bb6cb9d1b32.js.gz 545 bytes [emitted] [immutable]
js/85.app.035e53999bb6cb9d1b32.js 1020 bytes 85 [emitted] [immutable]
js/85.app.035e53999bb6cb9d1b32.js.br 509 bytes [emitted] [immutable]
js/85.app.035e53999bb6cb9d1b32.js.gz 591 bytes [emitted] [immutable]
js/86.app.035e53999bb6cb9d1b32.js 322 bytes 86 [emitted] [immutable]
js/86.app.035e53999bb6cb9d1b32.js.br 204 bytes [emitted] [immutable]
js/86.app.035e53999bb6cb9d1b32.js.gz 243 bytes [emitted] [immutable]
js/87.app.035e53999bb6cb9d1b32.js 964 bytes 87 [emitted] [immutable]
js/87.app.035e53999bb6cb9d1b32.js.br 495 bytes [emitted] [immutable]
js/87.app.035e53999bb6cb9d1b32.js.gz 567 bytes [emitted] [immutable]
js/88.app.035e53999bb6cb9d1b32.js 991 bytes 88 [emitted] [immutable]
js/88.app.035e53999bb6cb9d1b32.js.br 505 bytes [emitted] [immutable]
js/88.app.035e53999bb6cb9d1b32.js.gz 581 bytes [emitted] [immutable]
js/89.app.035e53999bb6cb9d1b32.js 991 bytes 89 [emitted] [immutable]
js/89.app.035e53999bb6cb9d1b32.js.br 505 bytes [emitted] [immutable]
js/89.app.035e53999bb6cb9d1b32.js.gz 585 bytes [emitted] [immutable]
js/9.app.035e53999bb6cb9d1b32.js 15.5 KiB 9 [emitted] [immutable] vendors~admin-config~admin-pages
js/9.app.035e53999bb6cb9d1b32.js.br 4.87 KiB [emitted] [immutable]
js/9.app.035e53999bb6cb9d1b32.js.gz 5.37 KiB [emitted] [immutable]
js/90.app.035e53999bb6cb9d1b32.js 979 bytes 90 [emitted] [immutable]
js/90.app.035e53999bb6cb9d1b32.js.br 499 bytes [emitted] [immutable]
js/90.app.035e53999bb6cb9d1b32.js.gz 574 bytes [emitted] [immutable]
js/91.app.035e53999bb6cb9d1b32.js 980 bytes 91 [emitted] [immutable]
js/91.app.035e53999bb6cb9d1b32.js.br 507 bytes [emitted] [immutable]
js/91.app.035e53999bb6cb9d1b32.js.gz 581 bytes [emitted] [immutable]
js/92.app.035e53999bb6cb9d1b32.js 1.29 KiB 92 [emitted] [immutable]
js/92.app.035e53999bb6cb9d1b32.js.br 625 bytes [emitted] [immutable]
js/92.app.035e53999bb6cb9d1b32.js.gz 682 bytes [emitted] [immutable]
js/93.app.035e53999bb6cb9d1b32.js 1.09 KiB 93 [emitted] [immutable]
js/93.app.035e53999bb6cb9d1b32.js.br 548 bytes [emitted] [immutable]
js/93.app.035e53999bb6cb9d1b32.js.gz 599 bytes [emitted] [immutable]
js/94.app.035e53999bb6cb9d1b32.js 1.18 KiB 94 [emitted] [immutable]
js/94.app.035e53999bb6cb9d1b32.js.br 506 bytes [emitted] [immutable]
js/94.app.035e53999bb6cb9d1b32.js.gz 582 bytes [emitted] [immutable]
js/95.app.035e53999bb6cb9d1b32.js 1.62 KiB 95 [emitted] [immutable]
js/95.app.035e53999bb6cb9d1b32.js.br 713 bytes [emitted] [immutable]
js/95.app.035e53999bb6cb9d1b32.js.gz 785 bytes [emitted] [immutable]
js/96.app.035e53999bb6cb9d1b32.js 984 bytes 96 [emitted] [immutable]
js/96.app.035e53999bb6cb9d1b32.js.br 517 bytes [emitted] [immutable]
js/96.app.035e53999bb6cb9d1b32.js.gz 576 bytes [emitted] [immutable]
js/97.app.035e53999bb6cb9d1b32.js 957 bytes 97 [emitted] [immutable]
js/97.app.035e53999bb6cb9d1b32.js.br 502 bytes [emitted] [immutable]
js/97.app.035e53999bb6cb9d1b32.js.gz 548 bytes [emitted] [immutable]
js/98.app.035e53999bb6cb9d1b32.js 969 bytes 98 [emitted] [immutable]
js/98.app.035e53999bb6cb9d1b32.js.br 508 bytes [emitted] [immutable]
js/98.app.035e53999bb6cb9d1b32.js.gz 557 bytes [emitted] [immutable]
js/99.app.035e53999bb6cb9d1b32.js 994 bytes 99 [emitted] [immutable]
js/99.app.035e53999bb6cb9d1b32.js.br 515 bytes [emitted] [immutable]
js/99.app.035e53999bb6cb9d1b32.js.gz 566 bytes [emitted] [immutable]
js/app.035e53999bb6cb9d1b32.js 1.7 MiB 29 [emitted] [immutable] main
js/app.035e53999bb6cb9d1b32.js.br 331 KiB [emitted] [immutable]
js/app.035e53999bb6cb9d1b32.js.gz 429 KiB [emitted] [immutable]
manifest.json 887 bytes [emitted]
media/oh-sipclient-ringback.mp3 15.2 KiB [emitted]
media/oh-sipclient-ringtone.mp3 280 KiB [emitted]
precache-manifest.3d5c034b887b1c4de675b2ec11e23d07.js 42.7 KiB [emitted]
res/icons/128x128.png 3.75 KiB [emitted]
res/icons/144x144.png 4.19 KiB [emitted]
res/icons/152x152.png 4.53 KiB [emitted]
res/icons/192x192.png 5.65 KiB [emitted]
res/icons/256x256.png 7.82 KiB [emitted]
res/icons/512x512.png 17.5 KiB [emitted]
res/icons/apple-touch-icon.png 2.78 KiB [emitted]
res/icons/favicon.svg 725 bytes [emitted]
res/img/basicui.png 17.1 KiB [emitted]
res/img/cometvisu.png 41.3 KiB [emitted]
service-worker.js 329 bytes [emitted]
Entrypoint main = css/app.css js/app.035e53999bb6cb9d1b32.js
Build complete.
See the app entry points alone:
With some moving files around the totals are:
$ du -h js css
2.1M js/br
2.9M js/gz
9.5M js/uncompressed
15M js
124K css/br
159K css/gz
897K css/uncompressed
1.2M css
I remember trying this with the HABot client but it wasn't working well with the openHAB Cloud reverse proxy (it didn't forward the Content-Encoding
header IIRC).
@ghys I'll take a look at both use cases.
Regarding compression, for bundled gzip assets, does the client request something like app.js
, and then we would look for app.js.gz
and if thats found serve it, but indicate Content-Encoding: gzip
in the header? Or will the client ask for app.js.gz
?
For other file serving using gziped streams, i wonder, does something in our OSGI stack have any concept of this? is there something we could delegate to for this ? If not, its probably simple to implement, if we decide its worth doing.
Also i might look at using an eTag instead of the If-Modified header. Seems like it might be easier to deal with.
Regarding compression, for bundled gzip assets, does the client request something like app.js, and then we would look for app.js.gz and if thats found serve it, but indicate Content-Encoding: gzip in the header?
That's it.
For other file serving using gziped streams, i wonder, does something in our OSGI stack have any concept of this? is there something we could delegate to for this
Not sure, but it looks like Jetty has some support indeed. https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/handler/gzip/GzipHandler.html also in the DefaultServlet there's some interesting init parameters: https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/servlet/DefaultServlet.html
precompressed If set to a comma separated list of encoding types (that may be listed in a requests Accept-Encoding header) to file extension mappings to look for and serve. For example: "br=.br,gzip=.gz,bzip2=.bz". If set to a boolean True, then a default set of compressed formats will be used, otherwise no precompressed formats
Update- I've tried adding:
<Set name="gzip">true</Set>
<Set name="etags">true</Set>
to runtime/etc/jetty.xml
around here:
https://github.com/openhab/openhab-distro/blob/c4a1d03455cff53712d07ef0c86588097f02f1b7/distributions/openhab/src/main/resources/runtime/etc/jetty.xml#L51-L54
And stopped the main UI bundle so that the default servlet is used again for /static
, then placed multiple versions in conf/html
like so:
-rwxrwxrwx 1 ys ys 622568 Oct 10 01:29 app.css
-rwxrwxrwx 1 ys ys 68118 Oct 10 01:29 app.css.br
-rwxrwxrwx 1 ys ys 88949 Oct 10 01:29 app.css.gz
And indeed the gzip-encoded version is served and a ETag added:
On subsequent requests for this file Chrome sends a If-None-Match: W/"+feotJgJHBI+fepNycU9BM--gzip
request header and the server responds with a 304 as expected:
Note that the gzip paramater is deprecated and precompressedFormats should be used instead but it needs to be configured with an array in the xml. Definitely no need to reinvent the wheel then if we can use those features in the UI servlet.
NIce find!
looking now at https://git.eclipse.org/c/jetty/org.eclipse.jetty.project.git/tree/jettyservlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
,
looks promising!
So i have something working based on extending the jetty default servlet, i believe it actually solves caching, serving local gzip assets and byte range support..... and its actually less code then my original servlet .. and it worked the first time i tried running it....so something is certain to be wrong with it :-)
take a look at https://github.com/digitaldan/openhab-webui/blob/33dfddcd42bedb0705206fb449bb9fd0609b799f/bundles/org.openhab.ui/src/main/java/org/openhab/ui/internal/UIServlet.java , i have not tried acutally testing range or gzip files yet, but i believe they will work , i'll play some more with it this week.
I was hoping you'd come up with something like this, if it indeed works that's brilliant! This will give a nice performance boost when loading the UI (note that when the PWA features are available the service worker is likely to do its own caching, but it has to download the whole 11.34MB first - with GZip/Brotli we can bring it down to ~3MB/2.2MB!).
We have to make sure it plays well with reverse proxies in general and openHAB Cloud in particular, like if the response is compressed it should simply forward it to the client with the Content-Encoding
header and not try to decompress it. or strip the header. As mentioned above I had some problems with HABot where the content would arrive GZipped but without the header, maybe that's not the case anymore. Same thing for caching-related request & response headers like ETag
and If-None-Match
, they have to pass through the proxy.
@ghys do you have a branch which uses the webpack compression plugin ? I am running my test branch now on my production home system, so far so good, would like to test the gzipped assets. I'll also throw in a big video file in my html folder and see if Safari can stream it ( I believe thats the best way to test byte range support?)
No, but it's quite easy, just install:
$ npm install compression-webpack-plugin@6.0.3 --save-dev
and add this in webpack.config.js before ...(process.env.WEBPACK_ANALYZER ? [
(line 239):
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 0,
minRatio: Infinity,
}),
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11,
},
threshold: 0,
minRatio: Infinity,
}),
(import it with const CompressionPlugin = require('compression-webpack-plugin')
at the top)
( I believe thats the best way to test byte range support?)
Yes I suppose! Or you can test with curl on a text file perhaps: https://everything.curl.dev/http/ranges
Success! The servlet is successfully serving br and/or gz files automatically as discussed, caching using etags as well as byte range support. I'm running this on my home system, both locally, remote and through myopenHAB and everything is working. The PR is updated with this new servlet if you want to give it a try. The code is fairly simple as well and does not stray much from your original logic of serving resources which is a nice bonus since that has been throughly tested.
I'll play with it more this week to see if any tweaks are needed. I also want to double check we are passing all headers along through myopenhab to ensure optimal caching is happening.
Actually, myopenhab is respecting headers and compressed content, so a win there as well!
A huge step forward, unfortunately there seems to be some problems:
I tried to figure out what was the logic by analyzing https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java#L320 and https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java#L356 but
It would be better to have some kind of server preference (which seems to be the case: https://github.com/eclipse/jetty.project/commit/aa8597c19e6cb85d614db33a091946ae163a124b) but I haven't made sense of it. It really seems random based on the first resource it finds... Obviously if Brotli is available and supported by the browser it should be preferred.
Actually, myopenhab is respecting headers and compressed content, so a win there as well!
When the content is gzipped it actually seems that the content is sent uncompressed with a Content-Encoding: gzip
header, which for obvious reasons fails to decode.
This can be confirmed with some curl commands:
(works on local, response is gzipped & ungzipped by curl)
$ curl -k -v https://192.168.1.123:8443/js/app.9e494f674314ce5c7e79.js --compressed
...JS content...
(doesn't work on myopenhab.org)
$ curl -v --user 'myopenhab@domain.com' 'https://home.myopenhab.org/js/app.9e494f674314ce5c7e79.js' --compressed
Enter host password for user 'myopenhab@domain.com':
(...)
> Accept-Encoding: deflate, gzip, br
(...)
< Content-Encoding: gzip
curl: (61) Error while processing content unencoding: incorrect header check
(works by specifying the header manually, the response is said to be gzip-encoded but is actually not)
$ curl -v --user 'myopenhab@domain.com' 'https://home.myopenhab.org/js/app.9e494f674314ce5c7e79.js' -H 'Accept-Encoding: gzip, br'
(...)
> Accept-Encoding: gzip, br
(...)
< Content-Encoding: gzip
...JS content...
One quick & easy way to solve both 1. & 2. would be to only provide Brotli precompressed assets, as there doesn't seem to be any problems with them, they would be served as the only precompressed encoding available, and they have widespread support, worst case scenario the client would be served uncompressed assets.
It can probably be tracked to this code: https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java#L261
The redirect shouldn't happen and the index.html page should be served instead for those URLs - the UI's router will pick up the server-relative URL and display the proper page. Maybe the code just above (actually send the contents of /index.html as the "welcome" page when the resource is a directory) could solve this?
This is not a "real" problem, but it appears Jetty is serving a precompressed version (or not) at random,
Ok, i did see this once as well, but then it didn't happen, figured it was something weirdly cached on my end, but obviously not. I will need to play with this some more. I'll look at the code you referenced to see i can get a better picture of whats going on. Unfortunately this is not a well documented area of jetty.
FYI, If you turn up debug logging you can see how jetty calls for these files starting with the uncompressed version and moving to gz, and br.
07:30:09.941 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /index.html
07:30:09.942 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1178587240/app/index.html
07:30:09.943 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /index.html.br
07:30:09.944 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1178587240/app/index.html.br
07:30:09.945 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /index.html.gz
07:30:09.946 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1178587240/app/index.html.gz
When the content is gzipped it actually seems that the content is sent uncompressed with a Content-Encoding: gzip
Hmm, strange, all my files were served using br
encoding, so i did not actually get to see gzip, are you serving those special or out of the static directory? It did load for me using myopenhab, so let me check again, i may have jumped the gun assuming it was working fine.....fortunately i know a guy who can fix things over there if it not proxing right :-)
When you go to a page with an URL ending in `/', like https://localhost:8443/about/ or https://localhost:8443/settings/ and refresh the page, you end up with an error. It seems the servlet redirects to the URL without the last slash:
Yeah, i thought i took care of that, i even have a comment in the code specifying this very thing. So whats strange is that /page/page_6d44bbde74
works if you reload, as does /page/page_6d44bbde74/
, but not /settings
or /settings/
. I'll look at the bundle code again, it's probably a slight tweak to the logic.
This is not a "real" problem, but it appears Jetty is serving a precompressed version (or not) at random,
Correction, actually i did not see this exactly, but i did notice some strange behavior where sometimes it loads the single app bundle, and sometime it loads all of the fragments shown in your console. I'm not sure how that works with webpack? Does the client eventually need all the app fragments in the js directory, or can it load one big bundle ?
Thanks for the debug log tip, here's a typical refresh of the about page (with the unfortunate redirect):
20:14:04.956 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /about/
20:14:04.959 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/index.html
20:14:04.960 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /about/.br
20:14:04.962 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:04.964 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /about/.gz
20:14:04.966 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:04.974 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /about
20:14:04.976 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/index.html
20:14:04.978 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /about.br
20:14:04.980 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:04.982 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /about.gz
20:14:04.984 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:05.028 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /css/app.css
20:14:05.028 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /js/app.9e494f674314ce5c7e79.js
20:14:05.031 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/js/app.9e494f674314ce5c7e79.js
20:14:05.031 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/css/app.css
20:14:05.032 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /css/app.css.br
20:14:05.033 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /js/app.9e494f674314ce5c7e79.js.br
20:14:05.034 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/css/app.css.br
20:14:05.034 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/js/app.9e494f674314ce5c7e79.js.br
20:14:05.036 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /js/app.9e494f674314ce5c7e79.js.gz
20:14:05.037 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /css/app.css.gz
20:14:05.038 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/js/app.9e494f674314ce5c7e79.js.gz
20:14:05.040 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/css/app.css.gz
20:14:05.388 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /service-worker.js
20:14:05.390 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/service-worker.js
20:14:05.392 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /service-worker.js.br
20:14:05.393 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/service-worker.js.br
20:14:05.395 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /service-worker.js.gz
20:14:05.396 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/service-worker.js.gz
20:14:05.409 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /images/openhab-logo.svg
20:14:05.411 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo.svg
20:14:05.412 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /images/openhab-logo.svg.br
20:14:05.414 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo.svg.br
20:14:05.416 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /images/openhab-logo.svg.gz
20:14:05.417 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:05.443 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /images/openhab-logo-white.svg
20:14:05.444 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo-white.svg
20:14:05.446 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /images/openhab-logo-white.svg.br
20:14:05.448 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo-white.svg.br
20:14:05.449 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /images/openhab-logo-white.svg.gz
20:14:05.452 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:05.477 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /res/icons/favicon.svg
20:14:05.478 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/res/icons/favicon.svg
20:14:05.480 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /res/icons/favicon.svg.br
20:14:05.482 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/res/icons/favicon.svg.br
20:14:05.483 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /res/icons/favicon.svg.gz
20:14:05.486 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning null
20:14:05.579 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /js/86.app.9e494f674314ce5c7e79.js
20:14:05.581 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/js/86.app.9e494f674314ce5c7e79.js
20:14:05.582 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /js/86.app.9e494f674314ce5c7e79.js.br
20:14:05.584 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/js/86.app.9e494f674314ce5c7e79.js.br
20:14:05.586 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource: /js/86.app.9e494f674314ce5c7e79.js.gz
20:14:05.587 [DEBUG] [org.openhab.ui.internal.UIServlet ] - getResource returning bundleresource://217.fwk1864116663/app/js/86.app.9e494f674314ce5c7e79.js.gz
and the network traffic as seen by the browser:
I don't really see what determines that this app.9e494f674314ce5c7e79.js is served the gzip version instead of the brotli version.
fortunately i know a guy who can fix things over there if it not proxing right :-)
hehe, that's why I told you the details. It really seems that something in the middleware stack (express or nginx?) knows how to decode gzip but not brotli and decides to decode it.
Correction, actually i did not see this exactly, but i did notice some strange behavior where sometimes it loads the single app bundle, and sometime it loads all of the fragments shown in your console.
It depends on whether the service worker has successfully registered (in that case the workbox library will precache all assets, after which you will typically get a "install" or "add to home screen" prompt). Otherwise the assets will be loaded on demand depending on the needs - for example the SIP widget will load some additional assets. If you reload a simple page (for example the about page), the only UI assets that should be loaded will normally be the entry point, that is app.[hash].js and app.css (along with fonts and logos).
@ghys I am not seeing 'gz' assets served when 'br' assets are present. I have tried reloading and clearing cache many times as well as using both firefox and chrome. Any thoughts why its happening in your environment and how i might reproduce ?
@digitaldan Yes I think I figured out the symptom, but have no solution yet.
I'm putting all 3 versions of app.9e494f674314ce5c7e79.js (uncompressed, .gz, .br) from yesterday's builds and also the new one following your last commit (works nice btw) in conf/html
and try to fetch them with a browser.
For yesterday's build I always get the gz version but for today's I get the br version. If I delete the gz version from yesterday's build I get the uncompressed version (so the br version is completely ignored), likewise if I delete the br version from today's build, the gz version is ignored.
Turns out the critical line of code is this one: https://github.com/eclipse/jetty.project/blob/171b913242ec70d45e24dac65b8444f1cff70114/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java#L89
In particular: compressedResource.lastModified() >= resource.lastModified()
The compressed resource's last modified date has to be after the original resource's.
On yesterday's build this is not the case for the br asset (last modified 2s before the original):
:~/oh3/conf/html/1$ ls -l --full-time
total 2528
-rw-r--r-- 1 ys ys 1797646 2022-10-12 07:37:04.000000000 +0200 app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys 342226 2022-10-12 07:37:02.000000000 +0200 app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys 444575 2022-10-12 07:37:04.000000000 +0200 app.9e494f674314ce5c7e79.js.gz
On today's build the gz asset's last modified time is 4s before the original:
:~/oh3/conf/html/2$ ls -l --full-time
total 2528
-rw-r--r-- 1 ys ys 1797646 2022-10-13 21:13:38.000000000 +0200 app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys 342226 2022-10-13 21:13:38.000000000 +0200 app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys 444575 2022-10-13 21:13:34.000000000 +0200 app.9e494f674314ce5c7e79.js.gz
So that's why they are ignored.
The solution would be to ensure that the compressed assets always have a last modified date after the originals, but I'm not sure how to achieve that yet!
A faster way to check these last modified dates directly from the bundle is to look for the Last-Modified
HTTP header:
$ curl -I http://localhost:8080/js/app.9e494f674314ce5c7e79.js
HTTP/1.1 200 OK
Vary: Accept-Encoding
Last-Modified: Thu, 13 Oct 2022 19:13:38 GMT
Content-Type: application/javascript
ETag: W/"/q2XjcZHQ00/q2WDhSYG5M"
Accept-Ranges: bytes
Content-Length: 1797646
Server: Jetty(9.4.46.v20220331)
$ curl -I http://localhost:8080/js/app.9e494f674314ce5c7e79.js.gz
HTTP/1.1 200 OK
Last-Modified: Thu, 13 Oct 2022 19:13:34 GMT
Content-Type: application/gzip
ETag: W/"KmkRBvzQpdQKmkQhS4SSns"
Accept-Ranges: bytes
Content-Length: 444575
Server: Jetty(9.4.46.v20220331)
$ curl -I http://localhost:8080/js/app.9e494f674314ce5c7e79.js.br
HTTP/1.1 200 OK
Last-Modified: Thu, 13 Oct 2022 19:13:38 GMT
Content-Type: application/brotli
ETag: W/"KmkRBvzQtJEKmkQhS4RupM"
Accept-Ranges: bytes
Content-Length: 342226
Server: Jetty(9.4.46.v20220331)
Any compressed asset older than the original will never be served as described above.
In particular: compressedResource.lastModified() >= resource.lastModified()
Nice find! That would have taken me a while to figure out.
I have a couple ideas, but an easy one would be to extend the Resource object when loading out of the bundle, and for the last modified time always return the same date, like the startup time of the bundle, or grab it from maybe the index.html or something well known . So for example
private long startupTime startupTime = System.currentTimeMillis(); // or we get this from a well known file in the bundle.
....
class CommonTimeResource extends Resource {
private Resource baseResource;
public CustomResource(Resource baseResource) {
this.baseResource = baseResource;
}
// return the same time for all bundled files!
@Override
public long lastModified() {
return startupTime;
}
@Override
public boolean isContainedIn(@Nullable Resource r) throws MalformedURLException {
return baseResource.isContainedIn(r);
}
@Override
public void close() {
baseResource.close();
}
@Override
public boolean exists() {
return baseResource.exists();
}
@Override
public boolean isDirectory() {
return baseResource.isDirectory();
}
@Override
public long length() {
return baseResource.length();
}
@Override
public URL getURL() {
return baseResource.getURL();
}
@Override
public File getFile() throws IOException {
return baseResource.getFile();
}
@Override
public String getName() {
return baseResource.getName();
}
@Override
public InputStream getInputStream() throws IOException {
return baseResource.getInputStream();
}
@Override
public ReadableByteChannel getReadableByteChannel() throws IOException {
return baseResource.getReadableByteChannel();
}
@Override
public boolean delete() throws SecurityException {
return baseResource.delete();
}
@Override
public boolean renameTo(@Nullable Resource dest) throws SecurityException {
return baseResource.renameTo(dest);
}
@Override
public String[] list() {
return baseResource.list();
}
@Override
public Resource addPath(@Nullable String path) throws IOException, MalformedURLException {
return baseResource.addPath(path);
}
}
FYI I have not actually run this yet, so not sure it works. The other option would be to do something during the node build, but i worry about platform specific time issues depending on the build host and OS.
Yep the custom Resource could work! According to https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URLConnection.html#getLastModified() we can return "0 if not known".
The other option would be to do something during the node build, but i worry about platform specific time issues depending on the build host and OS.
Probably won't work because the dates in the .jar are actually all the same:
$ wget https://ci.openhab.org/job/PR-openHAB-WebUI/lastSuccessfulBuild/artifact/bundles/org.openhab.ui/target/org.openhab.ui-3.4.0-SNAPSHOT.jar
$ unzip org.openhab.ui-3.4.0-SNAPSHOT.jar
$ cd app/js
$ ls -l --full-time
-rw-r--r-- 1 ys ys 121180 2022-10-13 18:30:04.000000000 +0200 0.app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys 31439 2022-10-13 18:30:04.000000000 +0200 0.app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys 38034 2022-10-13 18:30:04.000000000 +0200 0.app.9e494f674314ce5c7e79.js.gz
...
-rw-r--r-- 1 ys ys 1797646 2022-10-13 18:30:04.000000000 +0200 app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys 342226 2022-10-13 18:30:04.000000000 +0200 app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys 444575 2022-10-13 18:30:04.000000000 +0200 app.9e494f674314ce5c7e79.js.gz
I'm not sure where these different last modified times are from, probably from a cache?
Give the latest push a try and let me know what you think !
It works well! Good idea to only use the resource wrapper for the bundle resources and not the user static assets (having the real time in the Last-Modified
header can help).
Note the odd uncompressed assets, this make sense - that's because of the other condition in Jetty's code, the compressed versions are ignored when they're larger in size than the originals, which is good.
Closes #1432. Closes #1171.
Thanks!
Note that the issue with openHAB Cloud uncompressing gzip and sending them plain with the wrong header remains
I'll be looking into that this weekend, its not clear if its the Java http client in the cloud binding who makes the final request to the servlet, something in node land, something in nginx and so on......
Great but it's not as critical in the end, a quick & easy suggestion to reproduce:
$ cd $OPENHAB_CONF/html
$ wget http://localhost:8080/css/app.css
$ wget http://localhost:8080/css/app.css.gz
$ curl -I http://localhost:8080/static/app.css --compressed
^ should have Content-Encoding: gzip
$ curl http://localhost:8080/static/app.css --compressed
^ should work
$ curl -I -u '<myopenhab username>' https://home.myopenhab.org/static/app.css --compressed
^ should have Content-Encoding: gzip
$ curl -u '<myopenhab username>' https://home.myopenhab.org/static/app.css --compressed
curl: (61) Error while processing content unencoding: incorrect header check
Yeah, was not sure if we wanted to support compression look up on static files. Its a one or two line change to the logic if indeed we want to support it .
Sorry forget my previous comment, i thought you were referencing something else. My hunch is that the Jetty HTTP client in the binding see's the the returned content encoding and decompresses it before returning its byte array, then we are passing it along but still specifying Content-Encoding: gzip. I think its an easy fix (🤞 )
Also, use hash in webpack builds to ensure unique names for caching.
Signed-off-by: Dan Cunningham dan@digitaldan.com