From 06cc22b10d41fde45c7ebc4b042dafe657400924 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 31 Jul 2018 23:21:34 -0400 Subject: [PATCH] detect unused data on initial render --- src/core/create_manifests.ts | 1 + src/middleware.ts | 50 ++++++++++++-- src/middleware/list_unused_properties.ts | 34 ++++++++++ src/middleware/wrap_data.ts | 85 ++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 src/middleware/list_unused_properties.ts create mode 100644 src/middleware/wrap_data.ts diff --git a/src/core/create_manifests.ts b/src/core/create_manifests.ts index b2149fd..4872a4e 100644 --- a/src/core/create_manifests.ts +++ b/src/core/create_manifests.ts @@ -156,6 +156,7 @@ function generate_server( const props = [ `name: "${part.component.name}"`, + `file: "${part.component.file}"`, `component: ${part.component.name}` ]; diff --git a/src/middleware.ts b/src/middleware.ts index bb2d0da..6e1f53e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -8,6 +8,9 @@ import fetch from 'node-fetch'; import { lookup } from './middleware/mime'; import { locations, dev } from './config'; import sourceMapSupport from 'source-map-support'; +import prettyBytes from 'pretty-bytes'; +import { wrap_data } from './middleware/wrap_data'; +import { list_unused_properties } from './middleware/list_unused_properties'; sourceMapSupport.install(); @@ -394,11 +397,6 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) return; } - const serialized = { - preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`, - store: store && try_serialize(store.get()) - }; - const segments = req.path.split('/').filter(Boolean); const props: Props = { @@ -420,6 +418,11 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) } }); + let { data: preloaded_proxies, unwrap } = wrap_data(preloaded); + + // this is an easy way to 'reify' top-level values + const reified = preloaded_proxies.map((x: any) => x); + let level = data.child; for (let i = 0; i < page.parts.length; i += 1) { const part = page.parts[i]; @@ -431,7 +434,7 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) component: part.component, props: Object.assign({}, props, { params: get_params(match) - }, preloaded[i + 1]) + }, reified[i + 1]) }); level.props.child = { @@ -450,6 +453,41 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store) .map(file => ``) .join(''); + const unwrapped = unwrap(); + + const preloaded_serialized = preloaded.map((data, i) => { + const all = try_serialize(data); + const used = try_serialize(unwrapped[i]); + + if (all !== used) { + const props = list_unused_properties(data, unwrapped[i]); + + const part = page.parts[i - 1] || { file: 'routes/_layout.html' }; + console.log(`${part.file} is preloading more data (${prettyBytes(all.length)}) than is initially rendered (${prettyBytes(used.length)}). The following properties are unused...`); + + const slice = props.length > 22 + ? props.slice(0, 20) + : props; + + console.log(slice.join('\n')); + + if (props.length > slice.length) { + console.log(`...and ${props.length - slice.length} more`); + } + } + + return all; + }); + + const serialized = { + preloaded: `[${preloaded_serialized.join(',')}]`, + store: store && try_serialize(store.get()) + }; + + if (serialized.preloaded.length > 10000) { + console.log(`preloaded data is ${prettyBytes(serialized.preloaded.length)}. that's too much!`) + } + let inline_script = `__SAPPER__={${[ error && `error:1`, `baseUrl:"${req.baseUrl}"`, diff --git a/src/middleware/list_unused_properties.ts b/src/middleware/list_unused_properties.ts new file mode 100644 index 0000000..4b147c4 --- /dev/null +++ b/src/middleware/list_unused_properties.ts @@ -0,0 +1,34 @@ +export function list_unused_properties(all: any, used: any) { + const props: string[] = []; + + const seen = new Set(); + + function walk(keypath: string, a: any, b: any) { + if (seen.has(a)) return; + seen.add(a); + + if (!a || typeof a !== 'object') return; + + const is_array = Array.isArray(a); + + for (const key in a) { + const child_keypath = keypath + ? is_array ? `${keypath}[${key}]` : `${keypath}.${key}` + : key; + + if (hasProp.call(b, key)) { + const a_child = a[key]; + const b_child = b[key]; + + walk(child_keypath, a_child, b_child); + } else { + props.push(child_keypath); + } + } + } + + walk(null, all, used); + return props; +} + +const hasProp = Object.prototype.hasOwnProperty; \ No newline at end of file diff --git a/src/middleware/wrap_data.ts b/src/middleware/wrap_data.ts new file mode 100644 index 0000000..23fb36a --- /dev/null +++ b/src/middleware/wrap_data.ts @@ -0,0 +1,85 @@ +type Obj = Record; + +export function wrap_data(data: any) { + const proxies = new Map(); + const clones = new Map(); + + const handler = { + get(target: any, property: string): any { + const value = target[property]; + const intercepted = intercept(value); + + const target_clone = clones.get(target); + const child_clone = clones.get(value); + + if (target_clone && target.hasOwnProperty(property)) { + target_clone[property] = child_clone || value; + } + + return intercepted; + }, + }; + + function get_or_create_proxy(obj: any) { + if (!proxies.has(obj)) { + proxies.set(obj, new Proxy(obj, handler)); + } + + return proxies.get(obj); + } + + function intercept(obj: any) { + if (clones.has(obj)) return obj; + + if (obj && typeof obj === 'object') { + if (Array.isArray(obj)) { + clones.set(obj, []); + return get_or_create_proxy(obj); + } + + else if (isPlainObject(obj)) { + clones.set(obj, {}); + return get_or_create_proxy(obj); + } + } + + clones.set(obj, obj); + return obj; + } + + return { + data: intercept(data), + unwrap: () => { + return clones.get(data); + } + }; +} + +const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0') + +function isPlainObject(obj: any) { + const proto = Object.getPrototypeOf(obj); + + if ( + proto !== Object.prototype && + proto !== null && + Object.getOwnPropertyNames(proto).sort().join('\0') !== objectProtoOwnPropertyNames + ) { + return false; + } + + if (Object.getOwnPropertySymbols(obj).length > 0) { + return false; + } + + return true; +} + + +function pick(obj: Obj, props: string[]) { + const picked: Obj = {}; + props.forEach(prop => { + picked[prop] = obj[prop]; + }); + return picked; +} \ No newline at end of file