perf: ~40ms improvement: Lazy load controllers and page components

This commit is contained in:
Feross Aboukhadijeh
2016-10-01 01:09:51 -07:00
parent 5815d8efe7
commit fcae064dbb
8 changed files with 107 additions and 87 deletions

View File

@@ -26,6 +26,7 @@
"drag-drop": "^2.12.1", "drag-drop": "^2.12.1",
"electron": "1.4.2", "electron": "1.4.2",
"es6-error": "^3.0.1", "es6-error": "^3.0.1",
"fn-getter": "^1.0.0",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"location-history": "^1.0.0", "location-history": "^1.0.0",

View File

@@ -2,10 +2,10 @@ module.exports = {
init init
} }
const config = require('./config')
const electron = require('electron')
function init () { function init () {
const config = require('./config')
const electron = require('electron')
electron.crashReporter.start({ electron.crashReporter.start({
companyName: config.APP_NAME, companyName: config.APP_NAME,
productName: config.APP_NAME, productName: config.APP_NAME,

View File

@@ -112,7 +112,7 @@ function dispatch (...args) {
function hide () { function hide () {
if (!main.win) return if (!main.win) return
main.win.send('dispatch', 'backToList') dispatch('backToList')
main.win.hide() main.win.hide()
} }

View File

@@ -9,7 +9,6 @@ const webtorrent = module.exports = {
const electron = require('electron') const electron = require('electron')
const config = require('../../config') const config = require('../../config')
const log = require('../log')
function init () { function init () {
const win = webtorrent.win = new electron.BrowserWindow({ const win = webtorrent.win = new electron.BrowserWindow({
@@ -52,7 +51,6 @@ function send (...args) {
function toggleDevTools () { function toggleDevTools () {
if (!webtorrent.win) return if (!webtorrent.win) return
log('toggleDevTools')
if (webtorrent.win.webContents.isDevToolsOpened()) { if (webtorrent.win.webContents.isDevToolsOpened()) {
webtorrent.win.webContents.closeDevTools() webtorrent.win.webContents.closeDevTools()
webtorrent.win.hide() webtorrent.win.hide()

View File

@@ -29,8 +29,6 @@ class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) } constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
} }
class InvalidTorrentError extends TorrentError {}
module.exports = { module.exports = {
CastingError, CastingError,
PlaybackError, PlaybackError,
@@ -39,6 +37,5 @@ module.exports = {
UnplayableTorrentError, UnplayableTorrentError,
UnplayableFileError, UnplayableFileError,
InvalidSoundNameError, InvalidSoundNameError,
TorrentKeyNotFoundError, TorrentKeyNotFoundError
InvalidTorrentError
} }

View File

@@ -28,6 +28,7 @@ crashReporter.init()
const State = require('./lib/state') const State = require('./lib/state')
State.load(onState) State.load(onState)
const createGetter = require('fn-getter')
const dragDrop = require('drag-drop') const dragDrop = require('drag-drop')
const electron = require('electron') const electron = require('electron')
const fs = require('fs') const fs = require('fs')
@@ -39,19 +40,13 @@ const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound') const sound = require('./lib/sound')
const TorrentPlayer = require('./lib/torrent-player') const TorrentPlayer = require('./lib/torrent-player')
const TorrentListController = require('./controllers/torrent-list-controller')
// Required by Material UI -- adds `onTouchTap` event // Required by Material UI -- adds `onTouchTap` event
require('react-tap-event-plugin')() require('react-tap-event-plugin')()
const App = require('./pages/app') const App = require('./pages/app')
const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller')
const PrefsController = require('./controllers/prefs-controller')
const PlaybackController = require('./controllers/playback-controller')
const SubtitlesController = require('./controllers/subtitles-controller')
const TorrentListController = require('./controllers/torrent-list-controller')
const TorrentController = require('./controllers/torrent-controller')
// Electron apps have two processes: a main process (node) runs first and starts // Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process, // a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process // and this IPC channel receives from and sends messages to the main process
@@ -93,13 +88,33 @@ function onState (err, _state) {
// Create controllers // Create controllers
controllers = { controllers = {
media: new MediaController(state), media: createGetter(() => {
update: new UpdateController(state), const MediaController = require('./controllers/media-controller')
prefs: new PrefsController(state, config), return new MediaController(state)
playback: new PlaybackController(state, config, update), }),
subtitles: new SubtitlesController(state), playback: createGetter(() => {
torrentList: new TorrentListController(state), const PlaybackController = require('./controllers/playback-controller')
torrent: new TorrentController(state) return new PlaybackController(state, config, update)
}),
prefs: createGetter(() => {
const PrefsController = require('./controllers/prefs-controller')
return new PrefsController(state, config)
}),
subtitles: createGetter(() => {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
}),
torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => {
const UpdateController = require('./controllers/update-controller')
return new UpdateController(state)
})
} }
// Add first page to location history // Add first page to location history
@@ -182,7 +197,7 @@ function lazyLoadCast () {
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller // 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
// 4. controller - the controller handles the event, changing the state object // 4. controller - the controller handles the event, changing the state object
function update () { function update () {
controllers.playback.showOrHidePlayerControls() controllers.playback().showOrHidePlayerControls()
app.setState(state) app.setState(state)
updateElectron() updateElectron()
} }
@@ -210,54 +225,54 @@ const dispatchHandlers = {
'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */ 'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } }, 'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId), 'addTorrent': (torrentId) => controllers.torrentList().addTorrent(torrentId),
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths), 'showCreateTorrent': (paths) => controllers.torrentList().showCreateTorrent(paths),
'createTorrent': (options) => controllers.torrentList.createTorrent(options), 'createTorrent': (options) => controllers.torrentList().createTorrent(options),
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash), 'toggleTorrent': (infoHash) => controllers.torrentList().toggleTorrent(infoHash),
'toggleTorrentFile': (infoHash, index) => 'toggleTorrentFile': (infoHash, index) =>
controllers.torrentList.toggleTorrentFile(infoHash, index), controllers.torrentList().toggleTorrentFile(infoHash, index),
'confirmDeleteTorrent': (infoHash, deleteData) => 'confirmDeleteTorrent': (infoHash, deleteData) =>
controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData), controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
'deleteTorrent': (infoHash, deleteData) => 'deleteTorrent': (infoHash, deleteData) =>
controllers.torrentList.deleteTorrent(infoHash, deleteData), controllers.torrentList().deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) => 'toggleSelectTorrent': (infoHash) =>
controllers.torrentList.toggleSelectTorrent(infoHash), controllers.torrentList().toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) => 'openTorrentContextMenu': (infoHash) =>
controllers.torrentList.openTorrentContextMenu(infoHash), controllers.torrentList().openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) => 'startTorrentingSummary': (torrentKey) =>
controllers.torrentList.startTorrentingSummary(torrentKey), controllers.torrentList().startTorrentingSummary(torrentKey),
'saveTorrentFileAs': (torrentKey) => 'saveTorrentFileAs': (torrentKey) =>
controllers.torrentList.saveTorrentFileAs(torrentKey), controllers.torrentList().saveTorrentFileAs(torrentKey),
// Playback // Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index), 'playFile': (infoHash, index) => controllers.playback().playFile(infoHash, index),
'playPause': () => controllers.playback.playPause(), 'playPause': () => controllers.playback().playPause(),
'nextTrack': () => controllers.playback.nextTrack(), 'nextTrack': () => controllers.playback().nextTrack(),
'previousTrack': () => controllers.playback.previousTrack(), 'previousTrack': () => controllers.playback().previousTrack(),
'skip': (time) => controllers.playback.skip(time), 'skip': (time) => controllers.playback().skip(time),
'skipTo': (time) => controllers.playback.skipTo(time), 'skipTo': (time) => controllers.playback().skipTo(time),
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir), 'changePlaybackRate': (dir) => controllers.playback().changePlaybackRate(dir),
'changeVolume': (delta) => controllers.playback.changeVolume(delta), 'changeVolume': (delta) => controllers.playback().changeVolume(delta),
'setVolume': (vol) => controllers.playback.setVolume(vol), 'setVolume': (vol) => controllers.playback().setVolume(vol),
'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index), 'openItem': (infoHash, index) => controllers.playback().openItem(infoHash, index),
// Subtitles // Subtitles
'openSubtitles': () => controllers.subtitles.openSubtitles(), 'openSubtitles': () => controllers.subtitles().openSubtitles(),
'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index), 'selectSubtitle': (index) => controllers.subtitles().selectSubtitle(index),
'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(), 'toggleSubtitlesMenu': () => controllers.subtitles().toggleSubtitlesMenu(),
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(), 'checkForSubtitles': () => controllers.subtitles().checkForSubtitles(),
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect), 'addSubtitles': (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect),
// Local media: <video>, <audio>, external players // Local media: <video>, <audio>, external players
'mediaStalled': () => controllers.media.mediaStalled(), 'mediaStalled': () => controllers.media().mediaStalled(),
'mediaError': (err) => controllers.media.mediaError(err), 'mediaError': (err) => controllers.media().mediaError(err),
'mediaSuccess': () => controllers.media.mediaSuccess(), 'mediaSuccess': () => controllers.media().mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(), 'mediaTimeUpdate': () => controllers.media().mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(), 'mediaMouseMoved': () => controllers.media().mediaMouseMoved(),
'mediaControlsMouseEnter': () => controllers.media.controlsMouseEnter(), 'mediaControlsMouseEnter': () => controllers.media().controlsMouseEnter(),
'mediaControlsMouseLeave': () => controllers.media.controlsMouseLeave(), 'mediaControlsMouseLeave': () => controllers.media().controlsMouseLeave(),
'openExternalPlayer': () => controllers.media.openExternalPlayer(), 'openExternalPlayer': () => controllers.media().openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(), 'externalPlayerNotFound': () => controllers.media().externalPlayerNotFound(),
// Remote casting: Chromecast, Airplay, etc // Remote casting: Chromecast, Airplay, etc
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType), 'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
@@ -265,13 +280,13 @@ const dispatchHandlers = {
'stopCasting': () => lazyLoadCast().stop(), 'stopCasting': () => lazyLoadCast().stop(),
// Preferences screen // Preferences screen
'preferences': () => controllers.prefs.show(), 'preferences': () => controllers.prefs().show(),
'updatePreferences': (key, value) => controllers.prefs.update(key, value), 'updatePreferences': (key, value) => controllers.prefs().update(key, value),
'checkDownloadPath': checkDownloadPath, 'checkDownloadPath': checkDownloadPath,
// Update (check for new versions on Linux, where there's no auto updater) // Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update.updateAvailable(version), 'updateAvailable': (version) => controllers.update().updateAvailable(version),
'skipVersion': (version) => controllers.update.skipVersion(version), 'skipVersion': (version) => controllers.update().skipVersion(version),
// Navigation between screens (back, forward, ESC, etc) // Navigation between screens (back, forward, ESC, etc)
'exitModal': () => { state.modal = null }, 'exitModal': () => { state.modal = null },
@@ -309,7 +324,7 @@ function dispatch (action, ...args) {
// Update the virtual DOM, unless it's just a mouse move event // Update the virtual DOM, unless it's just a mouse move event
if (action !== 'mediaMouseMoved' || if (action !== 'mediaMouseMoved' ||
controllers.playback.showOrHidePlayerControls()) { controllers.playback().showOrHidePlayerControls()) {
update() update()
} }
} }
@@ -324,7 +339,7 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', onFullscreenChanged) ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged) ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
const tc = controllers.torrent const tc = controllers.torrent()
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args)) ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args)) ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args)) ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
@@ -377,7 +392,7 @@ function resumeTorrents () {
return torrentSummary return torrentSummary
}) })
.filter((s) => s.status !== 'paused') .filter((s) => s.status !== 'paused')
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey)) .forEach((s) => controllers.torrentList().startTorrentingSummary(s.torrentKey))
} }
// Set window dimensions to match video dimensions or fill the screen // Set window dimensions to match video dimensions or fill the screen
@@ -426,20 +441,20 @@ function onOpen (files) {
const url = state.location.url() const url = state.location.url()
const allTorrents = files.every(TorrentPlayer.isTorrent) const allTorrents = files.every(TorrentPlayer.isTorrent)
const allSubtitles = files.every(controllers.subtitles.isSubtitle) const allSubtitles = files.every(controllers.subtitles().isSubtitle)
if (allTorrents) { if (allTorrents) {
// Drop torrents onto the app: go to home screen, add torrents, no matter what // Drop torrents onto the app: go to home screen, add torrents, no matter what
dispatch('backToList') dispatch('backToList')
// All .torrent files? Add them. // All .torrent files? Add them.
files.forEach((file) => controllers.torrentList.addTorrent(file)) files.forEach((file) => controllers.torrentList().addTorrent(file))
} else if (url === 'player' && allSubtitles) { } else if (url === 'player' && allSubtitles) {
// Drop subtitles onto a playing video: add subtitles // Drop subtitles onto a playing video: add subtitles
controllers.subtitles.addSubtitles(files, true) controllers.subtitles().addSubtitles(files, true)
} else if (url === 'home') { } else if (url === 'home') {
// Drop files onto home screen: show Create Torrent // Drop files onto home screen: show Create Torrent
state.modal = null state.modal = null
controllers.torrentList.showCreateTorrent(files) controllers.torrentList().showCreateTorrent(files)
} else { } else {
// Drop files onto any other screen: show error // Drop files onto any other screen: show error
return onError('Please go back to the torrent list before creating a new torrent.') return onError('Please go back to the torrent list before creating a new torrent.')
@@ -461,7 +476,8 @@ function onError (err) {
function onPaste (e) { function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return if (e.target.tagName.toLowerCase() === 'input') return
controllers.torrentList.addTorrent(electron.clipboard.readText()) controllers.torrentList().addTorrent(electron.clipboard.readText())
update() update()
} }

View File

@@ -1,32 +1,36 @@
const colors = require('material-ui/styles/colors') const colors = require('material-ui/styles/colors')
const createGetter = require('fn-getter')
const React = require('react') const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header') const Header = require('../components/header')
const TorrentListPage = require('./torrent-list-page')
const Views = { const Views = {
'home': require('./torrent-list-page'), 'home': createGetter(() => TorrentListPage),
'player': require('./player-page'), 'player': createGetter(() => require('./player-page')),
'create-torrent': require('./create-torrent-page'), 'create-torrent': createGetter(() => require('./create-torrent-page')),
'preferences': require('./preferences-page') 'preferences': createGetter(() => require('./preferences-page'))
} }
const Modals = { const Modals = {
'open-torrent-address-modal': require('../components/open-torrent-address-modal'), 'open-torrent-address-modal': createGetter(
'remove-torrent-modal': require('../components/remove-torrent-modal'), () => require('../components/open-torrent-address-modal')
'update-available-modal': require('../components/update-available-modal'), ),
'unsupported-media-modal': require('../components/unsupported-media-modal') 'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
} }
const fontFamily = process.platform === 'win32' const fontFamily = process.platform === 'win32'
? '"Segoe UI", sans-serif' ? '"Segoe UI", sans-serif'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif' : 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
lightBaseTheme.fontFamily = fontFamily
darkBaseTheme.fontFamily = fontFamily darkBaseTheme.fontFamily = fontFamily
darkBaseTheme.userAgent = false
darkBaseTheme.palette.primary1Color = colors.grey50 darkBaseTheme.palette.primary1Color = colors.grey50
darkBaseTheme.palette.primary2Color = colors.grey50 darkBaseTheme.palette.primary2Color = colors.grey50
darkBaseTheme.palette.primary3Color = colors.grey600 darkBaseTheme.palette.primary3Color = colors.grey600
@@ -88,7 +92,12 @@ class App extends React.Component {
getModal () { getModal () {
const state = this.props.state const state = this.props.state
if (!state.modal) return if (!state.modal) return
const ModalContents = Modals[state.modal.id]
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
lightBaseTheme.fontFamily = fontFamily
lightBaseTheme.userAgent = false
const ModalContents = Modals[state.modal.id]()
return ( return (
<MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}> <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}>
<div key='modal' className='modal'> <div key='modal' className='modal'>
@@ -103,7 +112,7 @@ class App extends React.Component {
getView () { getView () {
const state = this.props.state const state = this.props.state
const View = Views[state.location.url()] const View = Views[state.location.url()]()
return (<View state={state} />) return (<View state={state} />)
} }
} }

View File

@@ -7,7 +7,6 @@ const LinearProgress = require('material-ui/LinearProgress').default
const TorrentSummary = require('../lib/torrent-summary') const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player') const TorrentPlayer = require('../lib/torrent-player')
const {dispatcher} = require('../lib/dispatcher') const {dispatcher} = require('../lib/dispatcher')
const {InvalidTorrentError} = require('../lib/errors')
module.exports = class TorrentList extends React.Component { module.exports = class TorrentList extends React.Component {
render () { render () {
@@ -60,7 +59,7 @@ module.exports = class TorrentList extends React.Component {
const classes = ['torrent'] const classes = ['torrent']
if (isSelected) classes.push('selected') if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled') if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey') if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
return ( return (
<div <div
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)} id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}